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}