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}