001/*
002 * Copyright 2015-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2015-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) 2015-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            DeliverPasswordResetTokenExtendedRequest;
052import com.unboundid.ldap.sdk.unboundidds.extensions.
053            DeliverPasswordResetTokenExtendedResult;
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.StaticUtils;
060import com.unboundid.util.ThreadSafety;
061import com.unboundid.util.ThreadSafetyLevel;
062import com.unboundid.util.args.ArgumentException;
063import com.unboundid.util.args.ArgumentParser;
064import com.unboundid.util.args.DNArgument;
065import com.unboundid.util.args.StringArgument;
066
067import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*;
068
069
070
071/**
072 * This class provides a utility that may be used to request that the Directory
073 * Server deliver a single-use password reset token to a user through some
074 * out-of-band mechanism.
075 * <BR>
076 * <BLOCKQUOTE>
077 *   <B>NOTE:</B>  This class, and other classes within the
078 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
079 *   supported for use against Ping Identity, UnboundID, and
080 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
081 *   for proprietary functionality or for external specifications that are not
082 *   considered stable or mature enough to be guaranteed to work in an
083 *   interoperable way with other types of LDAP servers.
084 * </BLOCKQUOTE>
085 */
086@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
087public final class DeliverPasswordResetToken
088       extends LDAPCommandLineTool
089       implements Serializable
090{
091  /**
092   * The serial version UID for this serializable class.
093   */
094  private static final long serialVersionUID = 5793619963770997266L;
095
096
097
098  // The DN of the user to whom the password reset token should be sent.
099  @Nullable private DNArgument userDN;
100
101  // The text to include after the password reset token in the "compact"
102  // message.
103  @Nullable private StringArgument compactTextAfterToken;
104
105  // The text to include before the password reset token in the "compact"
106  // message.
107  @Nullable private StringArgument compactTextBeforeToken;
108
109  // The name of the mechanism through which the one-time password should be
110  // delivered.
111  @Nullable private StringArgument deliveryMechanism;
112
113  // The text to include after the password reset token in the "full" message.
114  @Nullable private StringArgument fullTextAfterToken;
115
116  // The text to include before the password reset token in the "full" message.
117  @Nullable private StringArgument fullTextBeforeToken;
118
119  // The subject to use for the message containing the delivered token.
120  @Nullable private StringArgument messageSubject;
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(@NotNull 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  @NotNull()
156  public static ResultCode main(@NotNull final String[] args,
157                                @Nullable final OutputStream outStream,
158                                @Nullable final OutputStream errStream)
159  {
160    final DeliverPasswordResetToken tool =
161         new DeliverPasswordResetToken(outStream, errStream);
162    return tool.runTool(args);
163  }
164
165
166
167  /**
168   * Creates a new instance of this tool.
169   *
170   * @param  outStream  The output stream to which standard out should be
171   *                    written.  It may be {@code null} if output should be
172   *                    suppressed.
173   * @param  errStream  The output stream to which standard error should be
174   *                    written.  It may be {@code null} if error messages
175   *                    should be suppressed.
176   */
177  public DeliverPasswordResetToken(@Nullable final OutputStream outStream,
178                                   @Nullable final OutputStream errStream)
179  {
180    super(outStream, errStream);
181
182    userDN                 = null;
183    compactTextAfterToken  = null;
184    compactTextBeforeToken = null;
185    deliveryMechanism      = null;
186    fullTextAfterToken     = null;
187    fullTextBeforeToken    = null;
188    messageSubject         = null;
189  }
190
191
192
193  /**
194   * {@inheritDoc}
195   */
196  @Override()
197  @NotNull()
198  public String getToolName()
199  {
200    return "deliver-password-reset-token";
201  }
202
203
204
205  /**
206   * {@inheritDoc}
207   */
208  @Override()
209  @NotNull()
210  public String getToolDescription()
211  {
212    return INFO_DELIVER_PW_RESET_TOKEN_TOOL_DESCRIPTION.get();
213  }
214
215
216
217  /**
218   * {@inheritDoc}
219   */
220  @Override()
221  @NotNull()
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(@NotNull final ArgumentParser parser)
234         throws ArgumentException
235  {
236    userDN = new DNArgument('b', "userDN", true, 1,
237         INFO_DELIVER_PW_RESET_TOKEN_PLACEHOLDER_DN.get(),
238         INFO_DELIVER_PW_RESET_TOKEN_DESCRIPTION_USER_DN.get());
239    userDN.setArgumentGroupName(INFO_DELIVER_PW_RESET_TOKEN_GROUP_ID.get());
240    userDN.addLongIdentifier("user-dn", true);
241    parser.addArgument(userDN);
242
243    deliveryMechanism = new StringArgument('m', "deliveryMechanism", false, 0,
244         INFO_DELIVER_PW_RESET_TOKEN_PLACEHOLDER_NAME.get(),
245         INFO_DELIVER_PW_RESET_TOKEN_DESCRIPTION_MECH.get());
246    deliveryMechanism.setArgumentGroupName(
247         INFO_DELIVER_PW_RESET_TOKEN_GROUP_DELIVERY_MECH.get());
248    deliveryMechanism.addLongIdentifier("delivery-mechanism", true);
249    parser.addArgument(deliveryMechanism);
250
251    messageSubject = new StringArgument('s', "messageSubject", false, 1,
252         INFO_DELIVER_PW_RESET_TOKEN_PLACEHOLDER_SUBJECT.get(),
253         INFO_DELIVER_PW_RESET_TOKEN_DESCRIPTION_SUBJECT.get());
254    messageSubject.setArgumentGroupName(
255         INFO_DELIVER_PW_RESET_TOKEN_GROUP_DELIVERY_MECH.get());
256    messageSubject.addLongIdentifier("message-subject", true);
257    parser.addArgument(messageSubject);
258
259    fullTextBeforeToken = new StringArgument('f', "fullTextBeforeToken", false,
260         1, INFO_DELIVER_PW_RESET_TOKEN_PLACEHOLDER_FULL_BEFORE.get(),
261         INFO_DELIVER_PW_RESET_TOKEN_DESCRIPTION_FULL_BEFORE.get());
262    fullTextBeforeToken.setArgumentGroupName(
263         INFO_DELIVER_PW_RESET_TOKEN_GROUP_DELIVERY_MECH.get());
264    fullTextBeforeToken.addLongIdentifier("full-text-before-token", true);
265    parser.addArgument(fullTextBeforeToken);
266
267    fullTextAfterToken = new StringArgument('F', "fullTextAfterToken", false,
268         1, INFO_DELIVER_PW_RESET_TOKEN_PLACEHOLDER_FULL_AFTER.get(),
269         INFO_DELIVER_PW_RESET_TOKEN_DESCRIPTION_FULL_AFTER.get());
270    fullTextAfterToken.setArgumentGroupName(
271         INFO_DELIVER_PW_RESET_TOKEN_GROUP_DELIVERY_MECH.get());
272    fullTextAfterToken.addLongIdentifier("full-text-after-token", true);
273    parser.addArgument(fullTextAfterToken);
274
275    compactTextBeforeToken = new StringArgument('c', "compactTextBeforeToken",
276         false, 1, INFO_DELIVER_PW_RESET_TOKEN_PLACEHOLDER_COMPACT_BEFORE.get(),
277         INFO_DELIVER_PW_RESET_TOKEN_DESCRIPTION_COMPACT_BEFORE.get());
278    compactTextBeforeToken.setArgumentGroupName(
279         INFO_DELIVER_PW_RESET_TOKEN_GROUP_DELIVERY_MECH.get());
280    compactTextBeforeToken.addLongIdentifier("compact-text-before-token", true);
281    parser.addArgument(compactTextBeforeToken);
282
283    compactTextAfterToken = new StringArgument('C', "compactTextAfterToken",
284         false, 1, INFO_DELIVER_PW_RESET_TOKEN_PLACEHOLDER_COMPACT_AFTER.get(),
285         INFO_DELIVER_PW_RESET_TOKEN_DESCRIPTION_COMPACT_AFTER.get());
286    compactTextAfterToken.setArgumentGroupName(
287         INFO_DELIVER_PW_RESET_TOKEN_GROUP_DELIVERY_MECH.get());
288    compactTextAfterToken.addLongIdentifier("compact-text-after-token", true);
289    parser.addArgument(compactTextAfterToken);
290  }
291
292
293
294  /**
295   * {@inheritDoc}
296   */
297  @Override()
298  public boolean supportsInteractiveMode()
299  {
300    return true;
301  }
302
303
304
305  /**
306   * {@inheritDoc}
307   */
308  @Override()
309  public boolean defaultsToInteractiveMode()
310  {
311    return true;
312  }
313
314
315
316  /**
317   * {@inheritDoc}
318   */
319  @Override()
320  protected boolean supportsOutputFile()
321  {
322    return true;
323  }
324
325
326
327  /**
328   * {@inheritDoc}
329   */
330  @Override()
331  protected boolean defaultToPromptForBindPassword()
332  {
333    return true;
334  }
335
336
337
338  /**
339   * Indicates whether this tool supports the use of a properties file for
340   * specifying default values for arguments that aren't specified on the
341   * command line.
342   *
343   * @return  {@code true} if this tool supports the use of a properties file
344   *          for specifying default values for arguments that aren't specified
345   *          on the command line, or {@code false} if not.
346   */
347  @Override()
348  public boolean supportsPropertiesFile()
349  {
350    return true;
351  }
352
353
354
355  /**
356   * Indicates whether the LDAP-specific arguments should include alternate
357   * versions of all long identifiers that consist of multiple words so that
358   * they are available in both camelCase and dash-separated versions.
359   *
360   * @return  {@code true} if this tool should provide multiple versions of
361   *          long identifiers for LDAP-specific arguments, or {@code false} if
362   *          not.
363   */
364  @Override()
365  protected boolean includeAlternateLongIdentifiers()
366  {
367    return true;
368  }
369
370
371
372  /**
373   * Indicates whether this tool should provide a command-line argument that
374   * allows for low-level SSL debugging.  If this returns {@code true}, then an
375   * "--enableSSLDebugging}" argument will be added that sets the
376   * "javax.net.debug" system property to "all" before attempting any
377   * communication.
378   *
379   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
380   *          argument, or {@code false} if not.
381   */
382  @Override()
383  protected boolean supportsSSLDebugging()
384  {
385    return true;
386  }
387
388
389
390  /**
391   * {@inheritDoc}
392   */
393  @Override()
394  protected boolean logToolInvocationByDefault()
395  {
396    return true;
397  }
398
399
400
401  /**
402   * {@inheritDoc}
403   */
404  @Override()
405  @NotNull()
406  public ResultCode doToolProcessing()
407  {
408    // Get the set of preferred delivery mechanisms.
409    final ArrayList<ObjectPair<String,String>> preferredDeliveryMechanisms;
410    if (deliveryMechanism.isPresent())
411    {
412      final List<String> dmList = deliveryMechanism.getValues();
413      preferredDeliveryMechanisms = new ArrayList<>(dmList.size());
414      for (final String s : dmList)
415      {
416        preferredDeliveryMechanisms.add(new ObjectPair<String,String>(s, null));
417      }
418    }
419    else
420    {
421      preferredDeliveryMechanisms = null;
422    }
423
424
425    // Get a connection to the directory server.
426    final LDAPConnection conn;
427    try
428    {
429      conn = getConnection();
430    }
431    catch (final LDAPException le)
432    {
433      Debug.debugException(le);
434      err(ERR_DELIVER_PW_RESET_TOKEN_CANNOT_GET_CONNECTION.get(
435           StaticUtils.getExceptionMessage(le)));
436      return le.getResultCode();
437    }
438
439    try
440    {
441      // Create and send the extended request
442      final DeliverPasswordResetTokenExtendedRequest request =
443           new DeliverPasswordResetTokenExtendedRequest(userDN.getStringValue(),
444                messageSubject.getValue(), fullTextBeforeToken.getValue(),
445                fullTextAfterToken.getValue(),
446                compactTextBeforeToken.getValue(),
447                compactTextAfterToken.getValue(), preferredDeliveryMechanisms);
448      final DeliverPasswordResetTokenExtendedResult result;
449      try
450      {
451        result = (DeliverPasswordResetTokenExtendedResult)
452             conn.processExtendedOperation(request);
453      }
454      catch (final LDAPException le)
455      {
456        Debug.debugException(le);
457        err(ERR_DELIVER_PW_RESET_TOKEN_ERROR_PROCESSING_EXTOP.get(
458             StaticUtils.getExceptionMessage(le)));
459        return le.getResultCode();
460      }
461
462      if (result.getResultCode() == ResultCode.SUCCESS)
463      {
464        final String mechanism = result.getDeliveryMechanism();
465        final String id = result.getRecipientID();
466        if (id == null)
467        {
468          out(INFO_DELIVER_PW_RESET_TOKEN_SUCCESS_RESULT_WITHOUT_ID.get(
469               mechanism));
470        }
471        else
472        {
473          out(INFO_DELIVER_PW_RESET_TOKEN_SUCCESS_RESULT_WITH_ID.get(mechanism,
474               id));
475        }
476
477        final String message = result.getDeliveryMessage();
478        if (message != null)
479        {
480          out(INFO_DELIVER_PW_RESET_TOKEN_SUCCESS_MESSAGE.get(message));
481        }
482      }
483      else
484      {
485        if (result.getDiagnosticMessage() == null)
486        {
487          err(ERR_DELIVER_PW_RESET_TOKEN_ERROR_RESULT_NO_MESSAGE.get(
488               String.valueOf(result.getResultCode())));
489        }
490        else
491        {
492          err(ERR_DELIVER_PW_RESET_TOKEN_ERROR_RESULT.get(
493               String.valueOf(result.getResultCode()),
494               result.getDiagnosticMessage()));
495        }
496      }
497
498      return result.getResultCode();
499    }
500    finally
501    {
502      conn.close();
503    }
504  }
505
506
507
508  /**
509   * {@inheritDoc}
510   */
511  @Override()
512  @NotNull()
513  public LinkedHashMap<String[],String> getExampleUsages()
514  {
515    final LinkedHashMap<String[],String> exampleMap =
516         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
517
518    final String[] args =
519    {
520      "--hostname", "server.example.com",
521      "--port", "389",
522      "--bindDN", "uid=password.admin,ou=People,dc=example,dc=com",
523      "--bindPassword", "password",
524      "--userDN", "uid=test.user,ou=People,dc=example,dc=com",
525      "--deliveryMechanism", "SMS",
526      "--deliveryMechanism", "E-Mail",
527      "--messageSubject", "Your password reset token",
528      "--fullTextBeforeToken", "Your single-use password reset token is '",
529      "--fullTextAfterToken", "'.",
530      "--compactTextBeforeToken", "Your single-use password reset token is '",
531      "--compactTextAfterToken", "'.",
532    };
533    exampleMap.put(args,
534         INFO_DELIVER_PW_RESET_TOKEN_EXAMPLE.get());
535
536    return exampleMap;
537  }
538}