001/* 002 * Copyright 2015-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2015-2023 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-2023 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}