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