001/* 002 * Copyright 2011-2022 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2011-2022 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) 2011-2022 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; 041 042import com.unboundid.asn1.ASN1Element; 043import com.unboundid.asn1.ASN1OctetString; 044import com.unboundid.asn1.ASN1Sequence; 045import com.unboundid.ldap.sdk.Control; 046import com.unboundid.ldap.sdk.LDAPException; 047import com.unboundid.ldap.sdk.ResultCode; 048import com.unboundid.util.Debug; 049import com.unboundid.util.NotMutable; 050import com.unboundid.util.NotNull; 051import com.unboundid.util.Nullable; 052import com.unboundid.util.StaticUtils; 053import com.unboundid.util.ThreadSafety; 054import com.unboundid.util.ThreadSafetyLevel; 055import com.unboundid.util.Validator; 056 057import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*; 058 059 060 061/** 062 * This class provides a request control that can be used by the client to 063 * identify the purpose of the associated operation. It can be used in 064 * conjunction with any kind of operation, and may be used to provide 065 * information about the reason for that operation, as well as about the client 066 * application used to generate the request. This may be very useful for 067 * debugging and auditing purposes. 068 * <BR> 069 * <BLOCKQUOTE> 070 * <B>NOTE:</B> This class, and other classes within the 071 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 072 * supported for use against Ping Identity, UnboundID, and 073 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 074 * for proprietary functionality or for external specifications that are not 075 * considered stable or mature enough to be guaranteed to work in an 076 * interoperable way with other types of LDAP servers. 077 * </BLOCKQUOTE> 078 * <BR> 079 * The criticality for this control may be either {@code true} or {@code false}. 080 * It must have a value with the following encoding: 081 * <PRE> 082 * OperationPurposeRequest ::= SEQUENCE { 083 * applicationName [0] OCTET STRING OPTIONAL, 084 * applicationVersion [1] OCTET STRING OPTIONAL, 085 * codeLocation [2] OCTET STRING OPTIONAL, 086 * requestPurpose [3] OCTET STRING OPTIONAL 087 * ... } 088 * </PRE> 089 * At least one of the elements in the value sequence must be present. 090 * <BR><BR> 091 * <H2>Example</H2> 092 * The following example demonstrates a sample authentication consisting of a 093 * search to find a user followed by a bind to verify that user's password. 094 * Both the search and bind requests will include operation purpose controls 095 * with information about the reason for the request. Note that for the sake 096 * of brevity and clarity, error handling has been omitted from this example. 097 * <PRE> 098 * SearchRequest searchRequest = new SearchRequest("dc=example,dc=com", 099 * SearchScope.SUB, Filter.createEqualityFilter("uid", uidValue), 100 * "1.1"); 101 * searchRequest.addControl(new OperationPurposeRequestControl(appName, 102 * appVersion, 0, "Retrieve the entry for a user with a given uid")); 103 * Entry userEntry = connection.searchForEntry(searchRequest); 104 * 105 * SimpleBindRequest bindRequest = new SimpleBindRequest(userEntry.getDN(), 106 * password, new OperationPurposeRequestControl(appName, appVersion, 0, 107 * "Bind as a user to verify the provided credentials.")); 108 * BindResult bindResult = connection.bind(bindRequest); 109 * </PRE> 110 */ 111@NotMutable() 112@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 113public final class OperationPurposeRequestControl 114 extends Control 115{ 116 /** 117 * The OID (1.3.6.1.4.1.30221.2.5.19) for the operation purpose request 118 * control. 119 */ 120 @NotNull public static final String OPERATION_PURPOSE_REQUEST_OID = 121 "1.3.6.1.4.1.30221.2.5.19"; 122 123 124 125 /** 126 * The BER type for the element that specifies the application name. 127 */ 128 private static final byte TYPE_APP_NAME = (byte) 0x80; 129 130 131 132 /** 133 * The BER type for the element that specifies the application version. 134 */ 135 private static final byte TYPE_APP_VERSION = (byte) 0x81; 136 137 138 139 /** 140 * The BER type for the element that specifies the code location. 141 */ 142 private static final byte TYPE_CODE_LOCATION = (byte) 0x82; 143 144 145 146 /** 147 * The BER type for the element that specifies the request purpose. 148 */ 149 private static final byte TYPE_REQUEST_PURPOSE = (byte) 0x83; 150 151 152 153 /** 154 * The serial version UID for this serializable class. 155 */ 156 private static final long serialVersionUID = -5552051862785419833L; 157 158 159 160 // The application name for this control, if any. 161 @Nullable private final String applicationName; 162 163 // The application version for this control, if any. 164 @Nullable private final String applicationVersion; 165 166 // The code location for this control, if any. 167 @Nullable private final String codeLocation; 168 169 // The request purpose for this control, if any. 170 @Nullable private final String requestPurpose; 171 172 173 174 /** 175 * Creates a new operation purpose request control with the provided 176 * information. It will not be critical. If the generateCodeLocation 177 * argument has a value of {@code false}, then at least one of the 178 * applicationName, applicationVersion, and requestPurpose arguments must 179 * be non-{@code null}. 180 * 181 * @param applicationName The name of the application generating the 182 * associated request. It may be {@code null} if 183 * this should not be included in the control. 184 * @param applicationVersion Information about the version of the 185 * application generating the associated request. 186 * It may be {@code null} if this should not be 187 * included in the control. 188 * @param codeLocationFrames Indicates that the code location should be 189 * automatically generated with a condensed stack 190 * trace for the current thread, using the 191 * specified number of stack frames. A value that 192 * is less than or equal to zero indicates an 193 * unlimited number of stack frames should be 194 * included. 195 * @param requestPurpose A string identifying the purpose of the 196 * associated request. It may be {@code null} if 197 * this should not be included in the control. 198 */ 199 public OperationPurposeRequestControl(@Nullable final String applicationName, 200 @Nullable final String applicationVersion, 201 final int codeLocationFrames, 202 @Nullable final String requestPurpose) 203 { 204 this(false, applicationName, applicationVersion, 205 generateStackTrace(codeLocationFrames), requestPurpose); 206 } 207 208 209 210 /** 211 * Creates a new operation purpose request control with the provided 212 * information. At least one of the applicationName, applicationVersion, 213 * codeLocation, and requestPurpose arguments must be non-{@code null}. 214 * 215 * @param isCritical Indicates whether the control should be 216 * considered critical. 217 * @param applicationName The name of the application generating the 218 * associated request. It may be {@code null} if 219 * this should not be included in the control. 220 * @param applicationVersion Information about the version of the 221 * application generating the associated request. 222 * It may be {@code null} if this should not be 223 * included in the control. 224 * @param codeLocation Information about the location in the 225 * application code in which the associated 226 * request is generated (e.g., the class and/or 227 * method name, or any other useful identifier). 228 * It may be {@code null} if this should not be 229 * included in the control. 230 * @param requestPurpose A string identifying the purpose of the 231 * associated request. It may be {@code null} if 232 * this should not be included in the control. 233 */ 234 public OperationPurposeRequestControl(final boolean isCritical, 235 @Nullable final String applicationName, 236 @Nullable final String applicationVersion, 237 @Nullable final String codeLocation, 238 @Nullable final String requestPurpose) 239 { 240 super(OPERATION_PURPOSE_REQUEST_OID, isCritical, 241 encodeValue(applicationName, applicationVersion, codeLocation, 242 requestPurpose)); 243 244 this.applicationName = applicationName; 245 this.applicationVersion = applicationVersion; 246 this.codeLocation = codeLocation; 247 this.requestPurpose = requestPurpose; 248 } 249 250 251 252 /** 253 * Creates a new operation purpose request control which is decoded from the 254 * provided generic control. 255 * 256 * @param control The generic control to be decoded as an operation purpose 257 * request control. 258 * 259 * @throws LDAPException If the provided control cannot be decoded as an 260 * operation purpose request control. 261 */ 262 public OperationPurposeRequestControl(@NotNull final Control control) 263 throws LDAPException 264 { 265 super(control); 266 267 final ASN1OctetString value = control.getValue(); 268 if (value == null) 269 { 270 throw new LDAPException(ResultCode.DECODING_ERROR, 271 ERR_OP_PURPOSE_NO_VALUE.get()); 272 } 273 274 final ASN1Element[] valueElements; 275 try 276 { 277 valueElements = 278 ASN1Sequence.decodeAsSequence(value.getValue()).elements(); 279 } 280 catch (final Exception e) 281 { 282 Debug.debugException(e); 283 throw new LDAPException(ResultCode.DECODING_ERROR, 284 ERR_OP_PURPOSE_VALUE_NOT_SEQUENCE.get( 285 StaticUtils.getExceptionMessage(e)), 286 e); 287 } 288 289 if (valueElements.length == 0) 290 { 291 throw new LDAPException(ResultCode.DECODING_ERROR, 292 ERR_OP_PURPOSE_VALUE_SEQUENCE_EMPTY.get()); 293 } 294 295 296 String appName = null; 297 String appVersion = null; 298 String codeLoc = null; 299 String reqPurpose = null; 300 for (final ASN1Element e : valueElements) 301 { 302 switch (e.getType()) 303 { 304 case TYPE_APP_NAME: 305 appName = ASN1OctetString.decodeAsOctetString(e).stringValue(); 306 break; 307 308 case TYPE_APP_VERSION: 309 appVersion = ASN1OctetString.decodeAsOctetString(e).stringValue(); 310 break; 311 312 case TYPE_CODE_LOCATION: 313 codeLoc = ASN1OctetString.decodeAsOctetString(e).stringValue(); 314 break; 315 316 case TYPE_REQUEST_PURPOSE: 317 reqPurpose = ASN1OctetString.decodeAsOctetString(e).stringValue(); 318 break; 319 320 default: 321 throw new LDAPException(ResultCode.DECODING_ERROR, 322 ERR_OP_PURPOSE_VALUE_UNSUPPORTED_ELEMENT.get( 323 StaticUtils.toHex(e.getType()))); 324 } 325 } 326 327 applicationName = appName; 328 applicationVersion = appVersion; 329 codeLocation = codeLoc; 330 requestPurpose = reqPurpose; 331 } 332 333 334 335 /** 336 * Generates a compact stack trace for the current thread, The stack trace 337 * elements will start with the last frame to call into this class (so that 338 * frames referencing this class, and anything called by this class in the 339 * process of getting the stack trace will be omitted). Elements will be 340 * space-delimited and will contain the unqualified class name, a period, 341 * the method name, a colon, and the source line number. 342 * 343 * @param numFrames The maximum number of frames to capture in the stack 344 * trace. 345 * 346 * @return The generated stack trace for the current thread. 347 */ 348 @NotNull() 349 private static String generateStackTrace(final int numFrames) 350 { 351 final StringBuilder buffer = new StringBuilder(); 352 final int n = (numFrames > 0) ? numFrames : Integer.MAX_VALUE; 353 354 int c = 0; 355 boolean skip = true; 356 for (final StackTraceElement e : Thread.currentThread().getStackTrace()) 357 { 358 final String className = e.getClassName(); 359 if (className.equals(OperationPurposeRequestControl.class.getName())) 360 { 361 skip = false; 362 continue; 363 } 364 else if (skip) 365 { 366 continue; 367 } 368 369 if (buffer.length() > 0) 370 { 371 buffer.append(' '); 372 } 373 374 final int lastPeriodPos = className.lastIndexOf('.'); 375 if (lastPeriodPos > 0) 376 { 377 buffer.append(className.substring(lastPeriodPos+1)); 378 } 379 else 380 { 381 buffer.append(className); 382 } 383 384 buffer.append('.'); 385 buffer.append(e.getMethodName()); 386 buffer.append(':'); 387 buffer.append(e.getLineNumber()); 388 389 c++; 390 if (c >= n) 391 { 392 break; 393 } 394 } 395 396 return buffer.toString(); 397 } 398 399 400 401 /** 402 * Encodes the provided information into a form suitable for use as the value 403 * of this control. 404 * 405 * @param applicationName The name of the application generating the 406 * associated request. It may be {@code null} if 407 * this should not be included in the control. 408 * @param applicationVersion Information about the version of the 409 * application generating the associated request. 410 * It may be {@code null} if this should not be 411 * included in the control. 412 * @param codeLocation Information about the location in the 413 * application code in which the associated 414 * request is generated (e.g., the class and/or 415 * method name, or any other useful identifier). 416 * It may be {@code null} if this should not be 417 * included in the control. 418 * @param requestPurpose A string identifying the purpose of the 419 * associated request. It may be {@code null} if 420 * this should not be included in the control. 421 * 422 * @return The encoded value for this control. 423 */ 424 @NotNull() 425 private static ASN1OctetString encodeValue( 426 @Nullable final String applicationName, 427 @Nullable final String applicationVersion, 428 @Nullable final String codeLocation, 429 @Nullable final String requestPurpose) 430 { 431 Validator.ensureFalse((applicationName == null) && 432 (applicationVersion == null) && (codeLocation == null) && 433 (requestPurpose == null)); 434 435 final ArrayList<ASN1Element> elements = new ArrayList<>(4); 436 437 if (applicationName != null) 438 { 439 elements.add(new ASN1OctetString(TYPE_APP_NAME, applicationName)); 440 } 441 442 if (applicationVersion != null) 443 { 444 elements.add(new ASN1OctetString(TYPE_APP_VERSION, applicationVersion)); 445 } 446 447 if (codeLocation != null) 448 { 449 elements.add(new ASN1OctetString(TYPE_CODE_LOCATION, codeLocation)); 450 } 451 452 if (requestPurpose != null) 453 { 454 elements.add(new ASN1OctetString(TYPE_REQUEST_PURPOSE, requestPurpose)); 455 } 456 457 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 458 } 459 460 461 462 /** 463 * Retrieves the name of the application that generated the associated 464 * request, if available. 465 * 466 * @return The name of the application that generated the associated request, 467 * or {@code null} if that is not available. 468 */ 469 @Nullable() 470 public String getApplicationName() 471 { 472 return applicationName; 473 } 474 475 476 477 /** 478 * Retrieves information about the version of the application that generated 479 * the associated request, if available. 480 * 481 * @return Information about the version of the application that generated 482 * the associated request, or {@code null} if that is not available. 483 */ 484 @Nullable() 485 public String getApplicationVersion() 486 { 487 return applicationVersion; 488 } 489 490 491 492 /** 493 * Retrieves information about the location in the application code in which 494 * the associated request was created, if available. 495 * 496 * @return Information about the location in the application code in which 497 * the associated request was created, or {@code null} if that is not 498 * available. 499 */ 500 @Nullable() 501 public String getCodeLocation() 502 { 503 return codeLocation; 504 } 505 506 507 508 /** 509 * Retrieves a message with information about the purpose of the associated 510 * request, if available. 511 * 512 * @return A message with information about the purpose of the associated 513 * request, or {@code null} if that is not available. 514 */ 515 @Nullable() 516 public String getRequestPurpose() 517 { 518 return requestPurpose; 519 } 520 521 522 523 /** 524 * {@inheritDoc} 525 */ 526 @Override() 527 @NotNull() 528 public String getControlName() 529 { 530 return INFO_CONTROL_NAME_OP_PURPOSE.get(); 531 } 532 533 534 535 /** 536 * {@inheritDoc} 537 */ 538 @Override() 539 public void toString(@NotNull final StringBuilder buffer) 540 { 541 buffer.append("OperationPurposeRequestControl(isCritical="); 542 buffer.append(isCritical()); 543 544 if (applicationName != null) 545 { 546 buffer.append(", appName='"); 547 buffer.append(applicationName); 548 buffer.append('\''); 549 } 550 551 552 if (applicationVersion != null) 553 { 554 buffer.append(", appVersion='"); 555 buffer.append(applicationVersion); 556 buffer.append('\''); 557 } 558 559 560 if (codeLocation != null) 561 { 562 buffer.append(", codeLocation='"); 563 buffer.append(codeLocation); 564 buffer.append('\''); 565 } 566 567 568 if (requestPurpose != null) 569 { 570 buffer.append(", purpose='"); 571 buffer.append(requestPurpose); 572 buffer.append('\''); 573 } 574 575 buffer.append(')'); 576 } 577}