001/* 002 * Copyright 2013-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2013-2023 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-2023 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 * Indicates whether the LDAP-specific arguments should include alternate 420 * versions of all long identifiers that consist of multiple words so that 421 * they are available in both camelCase and dash-separated versions. 422 * 423 * @return {@code true} if this tool should provide multiple versions of 424 * long identifiers for LDAP-specific arguments, or {@code false} if 425 * not. 426 */ 427 @Override() 428 protected boolean includeAlternateLongIdentifiers() 429 { 430 return true; 431 } 432 433 434 435 /** 436 * Indicates whether this tool should provide a command-line argument that 437 * allows for low-level SSL debugging. If this returns {@code true}, then an 438 * "--enableSSLDebugging}" argument will be added that sets the 439 * "javax.net.debug" system property to "all" before attempting any 440 * communication. 441 * 442 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 443 * argument, or {@code false} if not. 444 */ 445 @Override() 446 protected boolean supportsSSLDebugging() 447 { 448 return true; 449 } 450 451 452 453 /** 454 * {@inheritDoc} 455 */ 456 @Override() 457 protected boolean logToolInvocationByDefault() 458 { 459 return true; 460 } 461 462 463 464 /** 465 * {@inheritDoc} 466 */ 467 @Override() 468 @NotNull() 469 public ResultCode doToolProcessing() 470 { 471 // Construct the authentication identity. 472 final String authID; 473 if (bindDN.isPresent()) 474 { 475 authID = "dn:" + bindDN.getValue(); 476 } 477 else 478 { 479 authID = "u:" + userName.getValue(); 480 } 481 482 483 // Get the bind password. 484 final String pw; 485 if (bindPassword.isPresent()) 486 { 487 pw = bindPassword.getValue(); 488 } 489 else if (bindPasswordFile.isPresent()) 490 { 491 try 492 { 493 pw = new String(getPasswordFileReader().readPassword( 494 bindPasswordFile.getValue())); 495 } 496 catch (final Exception e) 497 { 498 Debug.debugException(e); 499 err(ERR_DELIVER_OTP_CANNOT_READ_BIND_PW.get( 500 StaticUtils.getExceptionMessage(e))); 501 return ResultCode.LOCAL_ERROR; 502 } 503 } 504 else 505 { 506 try 507 { 508 getOut().print(INFO_DELIVER_OTP_ENTER_PW.get()); 509 pw = StaticUtils.toUTF8String(PasswordReader.readPassword()); 510 getOut().println(); 511 } 512 catch (final Exception e) 513 { 514 Debug.debugException(e); 515 err(ERR_DELIVER_OTP_CANNOT_READ_BIND_PW.get( 516 StaticUtils.getExceptionMessage(e))); 517 return ResultCode.LOCAL_ERROR; 518 } 519 } 520 521 522 // Get the set of preferred delivery mechanisms. 523 final ArrayList<ObjectPair<String,String>> preferredDeliveryMechanisms; 524 if (deliveryMechanism.isPresent()) 525 { 526 final List<String> dmList = deliveryMechanism.getValues(); 527 preferredDeliveryMechanisms = new ArrayList<>(dmList.size()); 528 for (final String s : dmList) 529 { 530 preferredDeliveryMechanisms.add(new ObjectPair<String,String>(s, null)); 531 } 532 } 533 else 534 { 535 preferredDeliveryMechanisms = null; 536 } 537 538 539 // Get a connection to the directory server. 540 final LDAPConnection conn; 541 try 542 { 543 conn = getConnection(); 544 } 545 catch (final LDAPException le) 546 { 547 Debug.debugException(le); 548 err(ERR_DELIVER_OTP_CANNOT_GET_CONNECTION.get( 549 StaticUtils.getExceptionMessage(le))); 550 return le.getResultCode(); 551 } 552 553 try 554 { 555 // Create and send the extended request 556 final DeliverOneTimePasswordExtendedRequest request = 557 new DeliverOneTimePasswordExtendedRequest(authID, pw, 558 messageSubject.getValue(), fullTextBeforeOTP.getValue(), 559 fullTextAfterOTP.getValue(), compactTextBeforeOTP.getValue(), 560 compactTextAfterOTP.getValue(), preferredDeliveryMechanisms); 561 final DeliverOneTimePasswordExtendedResult result; 562 try 563 { 564 result = (DeliverOneTimePasswordExtendedResult) 565 conn.processExtendedOperation(request); 566 } 567 catch (final LDAPException le) 568 { 569 Debug.debugException(le); 570 err(ERR_DELIVER_OTP_ERROR_PROCESSING_EXTOP.get( 571 StaticUtils.getExceptionMessage(le))); 572 return le.getResultCode(); 573 } 574 575 if (result.getResultCode() == ResultCode.SUCCESS) 576 { 577 final String mechanism = result.getDeliveryMechanism(); 578 final String id = result.getRecipientID(); 579 if (id == null) 580 { 581 out(INFO_DELIVER_OTP_SUCCESS_RESULT_WITHOUT_ID.get(mechanism)); 582 } 583 else 584 { 585 out(INFO_DELIVER_OTP_SUCCESS_RESULT_WITH_ID.get(mechanism, id)); 586 } 587 588 final String message = result.getDeliveryMessage(); 589 if (message != null) 590 { 591 out(INFO_DELIVER_OTP_SUCCESS_MESSAGE.get(message)); 592 } 593 } 594 else 595 { 596 if (result.getDiagnosticMessage() == null) 597 { 598 err(ERR_DELIVER_OTP_ERROR_RESULT_NO_MESSAGE.get( 599 String.valueOf(result.getResultCode()))); 600 } 601 else 602 { 603 err(ERR_DELIVER_OTP_ERROR_RESULT.get( 604 String.valueOf(result.getResultCode()), 605 result.getDiagnosticMessage())); 606 } 607 } 608 609 return result.getResultCode(); 610 } 611 finally 612 { 613 conn.close(); 614 } 615 } 616 617 618 619 /** 620 * {@inheritDoc} 621 */ 622 @Override() 623 @NotNull() 624 public LinkedHashMap<String[],String> getExampleUsages() 625 { 626 final LinkedHashMap<String[],String> exampleMap = 627 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 628 629 String[] args = 630 { 631 "--hostname", "server.example.com", 632 "--port", "389", 633 "--bindDN", "uid=test.user,ou=People,dc=example,dc=com", 634 "--bindPassword", "password", 635 "--messageSubject", "Your one-time password", 636 "--fullTextBeforeOTP", "Your one-time password is '", 637 "--fullTextAfterOTP", "'.", 638 "--compactTextBeforeOTP", "Your OTP is '", 639 "--compactTextAfterOTP", "'.", 640 }; 641 exampleMap.put(args, 642 INFO_DELIVER_OTP_EXAMPLE_1.get()); 643 644 args = new String[] 645 { 646 "--hostname", "server.example.com", 647 "--port", "389", 648 "--userName", "test.user", 649 "--bindPassword", "password", 650 "--deliveryMechanism", "SMS", 651 "--deliveryMechanism", "E-Mail", 652 "--messageSubject", "Your one-time password", 653 "--fullTextBeforeOTP", "Your one-time password is '", 654 "--fullTextAfterOTP", "'.", 655 "--compactTextBeforeOTP", "Your OTP is '", 656 "--compactTextAfterOTP", "'.", 657 }; 658 exampleMap.put(args, 659 INFO_DELIVER_OTP_EXAMPLE_2.get()); 660 661 return exampleMap; 662 } 663}