001/* 002 * Copyright 2013-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2013-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) 2013-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; 037 038 039 040import java.io.OutputStream; 041import java.io.Serializable; 042import java.util.ArrayList; 043import java.util.LinkedHashMap; 044import java.util.List; 045 046import com.unboundid.ldap.sdk.LDAPConnection; 047import com.unboundid.ldap.sdk.LDAPException; 048import com.unboundid.ldap.sdk.ResultCode; 049import com.unboundid.ldap.sdk.Version; 050import com.unboundid.ldap.sdk.unboundidds.extensions. 051 DeliverOneTimePasswordExtendedRequest; 052import com.unboundid.ldap.sdk.unboundidds.extensions. 053 DeliverOneTimePasswordExtendedResult; 054import com.unboundid.util.Debug; 055import com.unboundid.util.LDAPCommandLineTool; 056import com.unboundid.util.NotNull; 057import com.unboundid.util.Nullable; 058import com.unboundid.util.ObjectPair; 059import com.unboundid.util.PasswordReader; 060import com.unboundid.util.StaticUtils; 061import com.unboundid.util.ThreadSafety; 062import com.unboundid.util.ThreadSafetyLevel; 063import com.unboundid.util.args.ArgumentException; 064import com.unboundid.util.args.ArgumentParser; 065import com.unboundid.util.args.BooleanArgument; 066import com.unboundid.util.args.DNArgument; 067import com.unboundid.util.args.FileArgument; 068import com.unboundid.util.args.StringArgument; 069 070import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*; 071 072 073 074/** 075 * This class provides a utility that may be used to request that the Directory 076 * Server deliver a one-time password to a user through some out-of-band 077 * mechanism. 078 * <BR> 079 * <BLOCKQUOTE> 080 * <B>NOTE:</B> This class, and other classes within the 081 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 082 * supported for use against Ping Identity, UnboundID, and 083 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 084 * for proprietary functionality or for external specifications that are not 085 * considered stable or mature enough to be guaranteed to work in an 086 * interoperable way with other types of LDAP servers. 087 * </BLOCKQUOTE> 088 */ 089@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 090public final class DeliverOneTimePassword 091 extends LDAPCommandLineTool 092 implements Serializable 093{ 094 /** 095 * The serial version UID for this serializable class. 096 */ 097 private static final long serialVersionUID = -7414730592661321416L; 098 099 100 101 // Indicates that the tool should interactively prompt the user for their 102 // bind password. 103 @Nullable private BooleanArgument promptForBindPassword; 104 105 // The DN for the user to whom the one-time password should be delivered. 106 @Nullable private DNArgument bindDN; 107 108 // The path to a file containing the static password for the user to whom the 109 // one-time password should be delivered. 110 @Nullable private FileArgument bindPasswordFile; 111 112 // The text to include after the one-time password in the "compact" message. 113 @Nullable private StringArgument compactTextAfterOTP; 114 115 // The text to include before the one-time password in the "compact" message. 116 @Nullable private StringArgument compactTextBeforeOTP; 117 118 // The name of the mechanism through which the one-time password should be 119 // delivered. 120 @Nullable private StringArgument deliveryMechanism; 121 122 // The text to include after the one-time password in the "full" message. 123 @Nullable private StringArgument fullTextAfterOTP; 124 125 // The text to include before the one-time password in the "full" message. 126 @Nullable private StringArgument fullTextBeforeOTP; 127 128 // The subject to use for the message containing the delivered token. 129 @Nullable private StringArgument messageSubject; 130 131 // The username for the user to whom the one-time password should be 132 // delivered. 133 @Nullable private StringArgument userName; 134 135 // The static password for the user to whom the one-time password should be 136 // delivered. 137 @Nullable private StringArgument bindPassword; 138 139 140 141 /** 142 * Parse the provided command line arguments and perform the appropriate 143 * processing. 144 * 145 * @param args The command line arguments provided to this program. 146 */ 147 public static void main(@NotNull final String... args) 148 { 149 final ResultCode resultCode = main(args, System.out, System.err); 150 if (resultCode != ResultCode.SUCCESS) 151 { 152 System.exit(resultCode.intValue()); 153 } 154 } 155 156 157 158 /** 159 * Parse the provided command line arguments and perform the appropriate 160 * processing. 161 * 162 * @param args The command line arguments provided to this program. 163 * @param outStream The output stream to which standard out should be 164 * written. It may be {@code null} if output should be 165 * suppressed. 166 * @param errStream The output stream to which standard error should be 167 * written. It may be {@code null} if error messages 168 * should be suppressed. 169 * 170 * @return A result code indicating whether the processing was successful. 171 */ 172 @NotNull() 173 public static ResultCode main(@NotNull final String[] args, 174 @Nullable final OutputStream outStream, 175 @Nullable final OutputStream errStream) 176 { 177 final DeliverOneTimePassword tool = 178 new DeliverOneTimePassword(outStream, errStream); 179 return tool.runTool(args); 180 } 181 182 183 184 /** 185 * Creates a new instance of this tool. 186 * 187 * @param outStream The output stream to which standard out should be 188 * written. It may be {@code null} if output should be 189 * suppressed. 190 * @param errStream The output stream to which standard error should be 191 * written. It may be {@code null} if error messages 192 * should be suppressed. 193 */ 194 public DeliverOneTimePassword(@Nullable final OutputStream outStream, 195 @Nullable final OutputStream errStream) 196 { 197 super(outStream, errStream); 198 199 promptForBindPassword = null; 200 bindDN = null; 201 bindPasswordFile = null; 202 bindPassword = null; 203 compactTextAfterOTP = null; 204 compactTextBeforeOTP = null; 205 deliveryMechanism = null; 206 fullTextAfterOTP = null; 207 fullTextBeforeOTP = null; 208 messageSubject = null; 209 userName = null; 210 } 211 212 213 214 /** 215 * {@inheritDoc} 216 */ 217 @Override() 218 @NotNull() 219 public String getToolName() 220 { 221 return "deliver-one-time-password"; 222 } 223 224 225 226 /** 227 * {@inheritDoc} 228 */ 229 @Override() 230 @NotNull() 231 public String getToolDescription() 232 { 233 return INFO_DELIVER_OTP_TOOL_DESCRIPTION.get(); 234 } 235 236 237 238 /** 239 * {@inheritDoc} 240 */ 241 @Override() 242 @NotNull() 243 public String getToolVersion() 244 { 245 return Version.NUMERIC_VERSION_STRING; 246 } 247 248 249 250 /** 251 * {@inheritDoc} 252 */ 253 @Override() 254 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 255 throws ArgumentException 256 { 257 bindDN = new DNArgument('D', "bindDN", false, 1, 258 INFO_DELIVER_OTP_PLACEHOLDER_DN.get(), 259 INFO_DELIVER_OTP_DESCRIPTION_BIND_DN.get()); 260 bindDN.setArgumentGroupName(INFO_DELIVER_OTP_GROUP_ID_AND_AUTH.get()); 261 bindDN.addLongIdentifier("bind-dn", true); 262 parser.addArgument(bindDN); 263 264 userName = new StringArgument('n', "userName", false, 1, 265 INFO_DELIVER_OTP_PLACEHOLDER_USERNAME.get(), 266 INFO_DELIVER_OTP_DESCRIPTION_USERNAME.get()); 267 userName.setArgumentGroupName(INFO_DELIVER_OTP_GROUP_ID_AND_AUTH.get()); 268 userName.addLongIdentifier("user-name", true); 269 parser.addArgument(userName); 270 271 bindPassword = new StringArgument('w', "bindPassword", false, 1, 272 INFO_DELIVER_OTP_PLACEHOLDER_PASSWORD.get(), 273 INFO_DELIVER_OTP_DESCRIPTION_BIND_PW.get()); 274 bindPassword.setSensitive(true); 275 bindPassword.setArgumentGroupName(INFO_DELIVER_OTP_GROUP_ID_AND_AUTH.get()); 276 bindPassword.addLongIdentifier("bind-password", true); 277 parser.addArgument(bindPassword); 278 279 bindPasswordFile = new FileArgument('j', "bindPasswordFile", false, 1, 280 INFO_DELIVER_OTP_PLACEHOLDER_PATH.get(), 281 INFO_DELIVER_OTP_DESCRIPTION_BIND_PW_FILE.get(), true, true, true, 282 false); 283 bindPasswordFile.setArgumentGroupName( 284 INFO_DELIVER_OTP_GROUP_ID_AND_AUTH.get()); 285 bindPasswordFile.addLongIdentifier("bind-password-file", true); 286 parser.addArgument(bindPasswordFile); 287 288 promptForBindPassword = new BooleanArgument(null, "promptForBindPassword", 289 1, INFO_DELIVER_OTP_DESCRIPTION_BIND_PW_PROMPT.get()); 290 promptForBindPassword.setArgumentGroupName( 291 INFO_DELIVER_OTP_GROUP_ID_AND_AUTH.get()); 292 promptForBindPassword.addLongIdentifier("prompt-for-bind-password", true); 293 parser.addArgument(promptForBindPassword); 294 295 deliveryMechanism = new StringArgument('m', "deliveryMechanism", false, 0, 296 INFO_DELIVER_OTP_PLACEHOLDER_NAME.get(), 297 INFO_DELIVER_OTP_DESCRIPTION_MECH.get()); 298 deliveryMechanism.setArgumentGroupName( 299 INFO_DELIVER_OTP_GROUP_DELIVERY_MECH.get()); 300 deliveryMechanism.addLongIdentifier("delivery-mechanism", true); 301 parser.addArgument(deliveryMechanism); 302 303 messageSubject = new StringArgument('s', "messageSubject", false, 1, 304 INFO_DELIVER_OTP_PLACEHOLDER_SUBJECT.get(), 305 INFO_DELIVER_OTP_DESCRIPTION_SUBJECT.get()); 306 messageSubject.setArgumentGroupName( 307 INFO_DELIVER_OTP_GROUP_DELIVERY_MECH.get()); 308 messageSubject.addLongIdentifier("message-subject", true); 309 parser.addArgument(messageSubject); 310 311 fullTextBeforeOTP = new StringArgument('f', "fullTextBeforeOTP", false, 312 1, INFO_DELIVER_OTP_PLACEHOLDER_FULL_BEFORE.get(), 313 INFO_DELIVER_OTP_DESCRIPTION_FULL_BEFORE.get()); 314 fullTextBeforeOTP.setArgumentGroupName( 315 INFO_DELIVER_OTP_GROUP_DELIVERY_MECH.get()); 316 fullTextBeforeOTP.addLongIdentifier("full-text-before-otp", true); 317 parser.addArgument(fullTextBeforeOTP); 318 319 fullTextAfterOTP = new StringArgument('F', "fullTextAfterOTP", false, 320 1, INFO_DELIVER_OTP_PLACEHOLDER_FULL_AFTER.get(), 321 INFO_DELIVER_OTP_DESCRIPTION_FULL_AFTER.get()); 322 fullTextAfterOTP.setArgumentGroupName( 323 INFO_DELIVER_OTP_GROUP_DELIVERY_MECH.get()); 324 fullTextAfterOTP.addLongIdentifier("full-text-after-otp", true); 325 parser.addArgument(fullTextAfterOTP); 326 327 compactTextBeforeOTP = new StringArgument('c', "compactTextBeforeOTP", 328 false, 1, INFO_DELIVER_OTP_PLACEHOLDER_COMPACT_BEFORE.get(), 329 INFO_DELIVER_OTP_DESCRIPTION_COMPACT_BEFORE.get()); 330 compactTextBeforeOTP.setArgumentGroupName( 331 INFO_DELIVER_OTP_GROUP_DELIVERY_MECH.get()); 332 compactTextBeforeOTP.addLongIdentifier("compact-text-before-otp", true); 333 parser.addArgument(compactTextBeforeOTP); 334 335 compactTextAfterOTP = new StringArgument('C', "compactTextAfterOTP", 336 false, 1, INFO_DELIVER_OTP_PLACEHOLDER_COMPACT_AFTER.get(), 337 INFO_DELIVER_OTP_DESCRIPTION_COMPACT_AFTER.get()); 338 compactTextAfterOTP.setArgumentGroupName( 339 INFO_DELIVER_OTP_GROUP_DELIVERY_MECH.get()); 340 compactTextAfterOTP.addLongIdentifier("compact-text-after-otp", true); 341 parser.addArgument(compactTextAfterOTP); 342 343 344 // Either the bind DN or username must have been provided. 345 parser.addRequiredArgumentSet(bindDN, userName); 346 347 // Only one option may be used for specifying the user identity. 348 parser.addExclusiveArgumentSet(bindDN, userName); 349 350 // Only one option may be used for specifying the bind password. 351 parser.addExclusiveArgumentSet(bindPassword, bindPasswordFile, 352 promptForBindPassword); 353 } 354 355 356 357 /** 358 * {@inheritDoc} 359 */ 360 @Override() 361 protected boolean supportsAuthentication() 362 { 363 return false; 364 } 365 366 367 368 /** 369 * {@inheritDoc} 370 */ 371 @Override() 372 public boolean supportsInteractiveMode() 373 { 374 return true; 375 } 376 377 378 379 /** 380 * {@inheritDoc} 381 */ 382 @Override() 383 public boolean defaultsToInteractiveMode() 384 { 385 return true; 386 } 387 388 389 390 /** 391 * {@inheritDoc} 392 */ 393 @Override() 394 protected boolean supportsOutputFile() 395 { 396 return true; 397 } 398 399 400 401 /** 402 * Indicates whether this tool supports the use of a properties file for 403 * specifying default values for arguments that aren't specified on the 404 * command line. 405 * 406 * @return {@code true} if this tool supports the use of a properties file 407 * for specifying default values for arguments that aren't specified 408 * on the command line, or {@code false} if not. 409 */ 410 @Override() 411 public boolean supportsPropertiesFile() 412 { 413 return true; 414 } 415 416 417 418 /** 419 * {@inheritDoc} 420 */ 421 @Override() 422 protected boolean supportsDebugLogging() 423 { 424 return true; 425 } 426 427 428 429 /** 430 * Indicates whether the LDAP-specific arguments should include alternate 431 * versions of all long identifiers that consist of multiple words so that 432 * they are available in both camelCase and dash-separated versions. 433 * 434 * @return {@code true} if this tool should provide multiple versions of 435 * long identifiers for LDAP-specific arguments, or {@code false} if 436 * not. 437 */ 438 @Override() 439 protected boolean includeAlternateLongIdentifiers() 440 { 441 return true; 442 } 443 444 445 446 /** 447 * Indicates whether this tool should provide a command-line argument that 448 * allows for low-level SSL debugging. If this returns {@code true}, then an 449 * "--enableSSLDebugging}" argument will be added that sets the 450 * "javax.net.debug" system property to "all" before attempting any 451 * communication. 452 * 453 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 454 * argument, or {@code false} if not. 455 */ 456 @Override() 457 protected boolean supportsSSLDebugging() 458 { 459 return true; 460 } 461 462 463 464 /** 465 * {@inheritDoc} 466 */ 467 @Override() 468 protected boolean logToolInvocationByDefault() 469 { 470 return true; 471 } 472 473 474 475 /** 476 * {@inheritDoc} 477 */ 478 @Override() 479 @NotNull() 480 public ResultCode doToolProcessing() 481 { 482 // Construct the authentication identity. 483 final String authID; 484 if (bindDN.isPresent()) 485 { 486 authID = "dn:" + bindDN.getValue(); 487 } 488 else 489 { 490 authID = "u:" + userName.getValue(); 491 } 492 493 494 // Get the bind password. 495 final String pw; 496 if (bindPassword.isPresent()) 497 { 498 pw = bindPassword.getValue(); 499 } 500 else if (bindPasswordFile.isPresent()) 501 { 502 try 503 { 504 pw = new String(getPasswordFileReader().readPassword( 505 bindPasswordFile.getValue())); 506 } 507 catch (final Exception e) 508 { 509 Debug.debugException(e); 510 err(ERR_DELIVER_OTP_CANNOT_READ_BIND_PW.get( 511 StaticUtils.getExceptionMessage(e))); 512 return ResultCode.LOCAL_ERROR; 513 } 514 } 515 else 516 { 517 try 518 { 519 getOut().print(INFO_DELIVER_OTP_ENTER_PW.get()); 520 pw = StaticUtils.toUTF8String(PasswordReader.readPassword()); 521 getOut().println(); 522 } 523 catch (final Exception e) 524 { 525 Debug.debugException(e); 526 err(ERR_DELIVER_OTP_CANNOT_READ_BIND_PW.get( 527 StaticUtils.getExceptionMessage(e))); 528 return ResultCode.LOCAL_ERROR; 529 } 530 } 531 532 533 // Get the set of preferred delivery mechanisms. 534 final ArrayList<ObjectPair<String,String>> preferredDeliveryMechanisms; 535 if (deliveryMechanism.isPresent()) 536 { 537 final List<String> dmList = deliveryMechanism.getValues(); 538 preferredDeliveryMechanisms = new ArrayList<>(dmList.size()); 539 for (final String s : dmList) 540 { 541 preferredDeliveryMechanisms.add(new ObjectPair<String,String>(s, null)); 542 } 543 } 544 else 545 { 546 preferredDeliveryMechanisms = null; 547 } 548 549 550 // Get a connection to the directory server. 551 final LDAPConnection conn; 552 try 553 { 554 conn = getConnection(); 555 } 556 catch (final LDAPException le) 557 { 558 Debug.debugException(le); 559 err(ERR_DELIVER_OTP_CANNOT_GET_CONNECTION.get( 560 StaticUtils.getExceptionMessage(le))); 561 return le.getResultCode(); 562 } 563 564 try 565 { 566 // Create and send the extended request 567 final DeliverOneTimePasswordExtendedRequest request = 568 new DeliverOneTimePasswordExtendedRequest(authID, pw, 569 messageSubject.getValue(), fullTextBeforeOTP.getValue(), 570 fullTextAfterOTP.getValue(), compactTextBeforeOTP.getValue(), 571 compactTextAfterOTP.getValue(), preferredDeliveryMechanisms); 572 final DeliverOneTimePasswordExtendedResult result; 573 try 574 { 575 result = (DeliverOneTimePasswordExtendedResult) 576 conn.processExtendedOperation(request); 577 } 578 catch (final LDAPException le) 579 { 580 Debug.debugException(le); 581 err(ERR_DELIVER_OTP_ERROR_PROCESSING_EXTOP.get( 582 StaticUtils.getExceptionMessage(le))); 583 return le.getResultCode(); 584 } 585 586 if (result.getResultCode() == ResultCode.SUCCESS) 587 { 588 final String mechanism = result.getDeliveryMechanism(); 589 final String id = result.getRecipientID(); 590 if (id == null) 591 { 592 out(INFO_DELIVER_OTP_SUCCESS_RESULT_WITHOUT_ID.get(mechanism)); 593 } 594 else 595 { 596 out(INFO_DELIVER_OTP_SUCCESS_RESULT_WITH_ID.get(mechanism, id)); 597 } 598 599 final String message = result.getDeliveryMessage(); 600 if (message != null) 601 { 602 out(INFO_DELIVER_OTP_SUCCESS_MESSAGE.get(message)); 603 } 604 } 605 else 606 { 607 if (result.getDiagnosticMessage() == null) 608 { 609 err(ERR_DELIVER_OTP_ERROR_RESULT_NO_MESSAGE.get( 610 String.valueOf(result.getResultCode()))); 611 } 612 else 613 { 614 err(ERR_DELIVER_OTP_ERROR_RESULT.get( 615 String.valueOf(result.getResultCode()), 616 result.getDiagnosticMessage())); 617 } 618 } 619 620 return result.getResultCode(); 621 } 622 finally 623 { 624 conn.close(); 625 } 626 } 627 628 629 630 /** 631 * {@inheritDoc} 632 */ 633 @Override() 634 @NotNull() 635 public LinkedHashMap<String[],String> getExampleUsages() 636 { 637 final LinkedHashMap<String[],String> exampleMap = 638 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 639 640 String[] args = 641 { 642 "--hostname", "server.example.com", 643 "--port", "389", 644 "--bindDN", "uid=test.user,ou=People,dc=example,dc=com", 645 "--bindPassword", "password", 646 "--messageSubject", "Your one-time password", 647 "--fullTextBeforeOTP", "Your one-time password is '", 648 "--fullTextAfterOTP", "'.", 649 "--compactTextBeforeOTP", "Your OTP is '", 650 "--compactTextAfterOTP", "'.", 651 }; 652 exampleMap.put(args, 653 INFO_DELIVER_OTP_EXAMPLE_1.get()); 654 655 args = new String[] 656 { 657 "--hostname", "server.example.com", 658 "--port", "389", 659 "--userName", "test.user", 660 "--bindPassword", "password", 661 "--deliveryMechanism", "SMS", 662 "--deliveryMechanism", "E-Mail", 663 "--messageSubject", "Your one-time password", 664 "--fullTextBeforeOTP", "Your one-time password is '", 665 "--fullTextAfterOTP", "'.", 666 "--compactTextBeforeOTP", "Your OTP is '", 667 "--compactTextAfterOTP", "'.", 668 }; 669 exampleMap.put(args, 670 INFO_DELIVER_OTP_EXAMPLE_2.get()); 671 672 return exampleMap; 673 } 674}