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}