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   * {@inheritDoc}
357   */
358  @Override()
359  protected boolean supportsDebugLogging()
360  {
361    return true;
362  }
363
364
365
366  /**
367   * Indicates whether the LDAP-specific arguments should include alternate
368   * versions of all long identifiers that consist of multiple words so that
369   * they are available in both camelCase and dash-separated versions.
370   *
371   * @return  {@code true} if this tool should provide multiple versions of
372   *          long identifiers for LDAP-specific arguments, or {@code false} if
373   *          not.
374   */
375  @Override()
376  protected boolean includeAlternateLongIdentifiers()
377  {
378    return true;
379  }
380
381
382
383  /**
384   * Indicates whether this tool should provide a command-line argument that
385   * allows for low-level SSL debugging.  If this returns {@code true}, then an
386   * "--enableSSLDebugging}" argument will be added that sets the
387   * "javax.net.debug" system property to "all" before attempting any
388   * communication.
389   *
390   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
391   *          argument, or {@code false} if not.
392   */
393  @Override()
394  protected boolean supportsSSLDebugging()
395  {
396    return true;
397  }
398
399
400
401  /**
402   * {@inheritDoc}
403   */
404  @Override()
405  protected boolean logToolInvocationByDefault()
406  {
407    return true;
408  }
409
410
411
412  /**
413   * {@inheritDoc}
414   */
415  @Override()
416  @NotNull()
417  public ResultCode doToolProcessing()
418  {
419    // Get the set of preferred delivery mechanisms.
420    final ArrayList<ObjectPair<String,String>> preferredDeliveryMechanisms;
421    if (deliveryMechanism.isPresent())
422    {
423      final List<String> dmList = deliveryMechanism.getValues();
424      preferredDeliveryMechanisms = new ArrayList<>(dmList.size());
425      for (final String s : dmList)
426      {
427        preferredDeliveryMechanisms.add(new ObjectPair<String,String>(s, null));
428      }
429    }
430    else
431    {
432      preferredDeliveryMechanisms = null;
433    }
434
435
436    // Get a connection to the directory server.
437    final LDAPConnection conn;
438    try
439    {
440      conn = getConnection();
441    }
442    catch (final LDAPException le)
443    {
444      Debug.debugException(le);
445      err(ERR_DELIVER_PW_RESET_TOKEN_CANNOT_GET_CONNECTION.get(
446           StaticUtils.getExceptionMessage(le)));
447      return le.getResultCode();
448    }
449
450    try
451    {
452      // Create and send the extended request
453      final DeliverPasswordResetTokenExtendedRequest request =
454           new DeliverPasswordResetTokenExtendedRequest(userDN.getStringValue(),
455                messageSubject.getValue(), fullTextBeforeToken.getValue(),
456                fullTextAfterToken.getValue(),
457                compactTextBeforeToken.getValue(),
458                compactTextAfterToken.getValue(), preferredDeliveryMechanisms);
459      final DeliverPasswordResetTokenExtendedResult result;
460      try
461      {
462        result = (DeliverPasswordResetTokenExtendedResult)
463             conn.processExtendedOperation(request);
464      }
465      catch (final LDAPException le)
466      {
467        Debug.debugException(le);
468        err(ERR_DELIVER_PW_RESET_TOKEN_ERROR_PROCESSING_EXTOP.get(
469             StaticUtils.getExceptionMessage(le)));
470        return le.getResultCode();
471      }
472
473      if (result.getResultCode() == ResultCode.SUCCESS)
474      {
475        final String mechanism = result.getDeliveryMechanism();
476        final String id = result.getRecipientID();
477        if (id == null)
478        {
479          out(INFO_DELIVER_PW_RESET_TOKEN_SUCCESS_RESULT_WITHOUT_ID.get(
480               mechanism));
481        }
482        else
483        {
484          out(INFO_DELIVER_PW_RESET_TOKEN_SUCCESS_RESULT_WITH_ID.get(mechanism,
485               id));
486        }
487
488        final String message = result.getDeliveryMessage();
489        if (message != null)
490        {
491          out(INFO_DELIVER_PW_RESET_TOKEN_SUCCESS_MESSAGE.get(message));
492        }
493      }
494      else
495      {
496        if (result.getDiagnosticMessage() == null)
497        {
498          err(ERR_DELIVER_PW_RESET_TOKEN_ERROR_RESULT_NO_MESSAGE.get(
499               String.valueOf(result.getResultCode())));
500        }
501        else
502        {
503          err(ERR_DELIVER_PW_RESET_TOKEN_ERROR_RESULT.get(
504               String.valueOf(result.getResultCode()),
505               result.getDiagnosticMessage()));
506        }
507      }
508
509      return result.getResultCode();
510    }
511    finally
512    {
513      conn.close();
514    }
515  }
516
517
518
519  /**
520   * {@inheritDoc}
521   */
522  @Override()
523  @NotNull()
524  public LinkedHashMap<String[],String> getExampleUsages()
525  {
526    final LinkedHashMap<String[],String> exampleMap =
527         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
528
529    final String[] args =
530    {
531      "--hostname", "server.example.com",
532      "--port", "389",
533      "--bindDN", "uid=password.admin,ou=People,dc=example,dc=com",
534      "--bindPassword", "password",
535      "--userDN", "uid=test.user,ou=People,dc=example,dc=com",
536      "--deliveryMechanism", "SMS",
537      "--deliveryMechanism", "E-Mail",
538      "--messageSubject", "Your password reset token",
539      "--fullTextBeforeToken", "Your single-use password reset token is '",
540      "--fullTextAfterToken", "'.",
541      "--compactTextBeforeToken", "Your single-use password reset token is '",
542      "--compactTextAfterToken", "'.",
543    };
544    exampleMap.put(args,
545         INFO_DELIVER_PW_RESET_TOKEN_EXAMPLE.get());
546
547    return exampleMap;
548  }
549}