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   * 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}