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