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