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