001/*
002 * Copyright 2016-2017 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2017 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.unboundidds;
022
023
024
025import java.io.BufferedReader;
026import java.io.FileReader;
027import java.io.OutputStream;
028import java.io.Serializable;
029import java.util.LinkedHashMap;
030
031import com.unboundid.ldap.sdk.ExtendedResult;
032import com.unboundid.ldap.sdk.LDAPConnection;
033import com.unboundid.ldap.sdk.LDAPException;
034import com.unboundid.ldap.sdk.ResultCode;
035import com.unboundid.ldap.sdk.Version;
036import com.unboundid.ldap.sdk.unboundidds.extensions.
037            DeregisterYubiKeyOTPDeviceExtendedRequest;
038import com.unboundid.ldap.sdk.unboundidds.extensions.
039            RegisterYubiKeyOTPDeviceExtendedRequest;
040import com.unboundid.util.Debug;
041import com.unboundid.util.LDAPCommandLineTool;
042import com.unboundid.util.PasswordReader;
043import com.unboundid.util.StaticUtils;
044import com.unboundid.util.ThreadSafety;
045import com.unboundid.util.ThreadSafetyLevel;
046import com.unboundid.util.args.ArgumentException;
047import com.unboundid.util.args.ArgumentParser;
048import com.unboundid.util.args.BooleanArgument;
049import com.unboundid.util.args.FileArgument;
050import com.unboundid.util.args.StringArgument;
051
052import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*;
053
054
055
056/**
057 * This class provides a utility that may be used to register a YubiKey OTP
058 * device for a specified user so that it may be used to authenticate that user.
059 * Alternately, it may be used to deregister one or all of the YubiKey OTP
060 * devices that have been registered for the user.
061 * <BR>
062 * <BLOCKQUOTE>
063 *   <B>NOTE:</B>  This class, and other classes within the
064 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
065 *   supported for use against Ping Identity, UnboundID, and Alcatel-Lucent 8661
066 *   server products.  These classes provide support for proprietary
067 *   functionality or for external specifications that are not considered stable
068 *   or mature enough to be guaranteed to work in an interoperable way with
069 *   other types of LDAP servers.
070 * </BLOCKQUOTE>
071 */
072@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
073public final class RegisterYubiKeyOTPDevice
074       extends LDAPCommandLineTool
075       implements Serializable
076{
077  /**
078   * The serial version UID for this serializable class.
079   */
080  private static final long serialVersionUID = 5705120716566064832L;
081
082
083
084  // Indicates that the tool should deregister one or all of the YubiKey OTP
085  // devices for the user rather than registering a new device.
086  private BooleanArgument deregister;
087
088  // Indicates that the tool should interactively prompt for the static password
089  // for the user for whom the YubiKey OTP device is to be registered or
090  // deregistered.
091  private BooleanArgument promptForUserPassword;
092
093  // The path to a file containing the static password for the user for whom the
094  // YubiKey OTP device is to be registered or deregistered.
095  private FileArgument userPasswordFile;
096
097  // The username for the user for whom the YubiKey OTP device is to be
098  // registered or deregistered.
099  private StringArgument authenticationID;
100
101  // The static password for the user for whom the YubiKey OTP device is to be
102  // registered or deregistered.
103  private StringArgument userPassword;
104
105  // A one-time password generated by the YubiKey OTP device to be registered
106  // or deregistered.
107  private StringArgument otp;
108
109
110
111  /**
112   * Parse the provided command line arguments and perform the appropriate
113   * processing.
114   *
115   * @param  args  The command line arguments provided to this program.
116   */
117  public static void main(final String... args)
118  {
119    final ResultCode resultCode = main(args, System.out, System.err);
120    if (resultCode != ResultCode.SUCCESS)
121    {
122      System.exit(resultCode.intValue());
123    }
124  }
125
126
127
128  /**
129   * Parse the provided command line arguments and perform the appropriate
130   * processing.
131   *
132   * @param  args       The command line arguments provided to this program.
133   * @param  outStream  The output stream to which standard out should be
134   *                    written.  It may be {@code null} if output should be
135   *                    suppressed.
136   * @param  errStream  The output stream to which standard error should be
137   *                    written.  It may be {@code null} if error messages
138   *                    should be suppressed.
139   *
140   * @return  A result code indicating whether the processing was successful.
141   */
142  public static ResultCode main(final String[] args,
143                                final OutputStream outStream,
144                                final OutputStream errStream)
145  {
146    final RegisterYubiKeyOTPDevice tool =
147         new RegisterYubiKeyOTPDevice(outStream, errStream);
148    return tool.runTool(args);
149  }
150
151
152
153  /**
154   * Creates a new instance of this tool.
155   *
156   * @param  outStream  The output stream to which standard out should be
157   *                    written.  It may be {@code null} if output should be
158   *                    suppressed.
159   * @param  errStream  The output stream to which standard error should be
160   *                    written.  It may be {@code null} if error messages
161   *                    should be suppressed.
162   */
163  public RegisterYubiKeyOTPDevice(final OutputStream outStream,
164                                  final OutputStream errStream)
165  {
166    super(outStream, errStream);
167
168    deregister            = null;
169    otp                   = null;
170    promptForUserPassword = null;
171    userPasswordFile      = null;
172    authenticationID      = null;
173    userPassword          = null;
174  }
175
176
177
178  /**
179   * {@inheritDoc}
180   */
181  @Override()
182  public String getToolName()
183  {
184    return "register-yubikey-otp-device";
185  }
186
187
188
189  /**
190   * {@inheritDoc}
191   */
192  @Override()
193  public String getToolDescription()
194  {
195    return INFO_REGISTER_YUBIKEY_OTP_DEVICE_TOOL_DESCRIPTION.get(
196         UnboundIDYubiKeyOTPBindRequest.UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME);
197  }
198
199
200
201  /**
202   * {@inheritDoc}
203   */
204  @Override()
205  public String getToolVersion()
206  {
207    return Version.NUMERIC_VERSION_STRING;
208  }
209
210
211
212  /**
213   * {@inheritDoc}
214   */
215  @Override()
216  public void addNonLDAPArguments(final ArgumentParser parser)
217         throws ArgumentException
218  {
219    deregister = new BooleanArgument(null, "deregister", 1,
220         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_DEREGISTER.get("--otp"));
221    deregister.addLongIdentifier("de-register");
222    parser.addArgument(deregister);
223
224    otp = new StringArgument(null, "otp", false, 1,
225         INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_OTP.get(),
226         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_OTP.get());
227    parser.addArgument(otp);
228
229    authenticationID = new StringArgument(null, "authID", false, 1,
230         INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_AUTHID.get(),
231         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_AUTHID.get());
232    authenticationID.addLongIdentifier("authenticationID");
233    authenticationID.addLongIdentifier("auth-id");
234    authenticationID.addLongIdentifier("authentication-id");
235    parser.addArgument(authenticationID);
236
237    userPassword = new StringArgument(null, "userPassword", false, 1,
238         INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_USER_PW.get(),
239         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_USER_PW.get(
240              authenticationID.getIdentifierString()));
241    userPassword.setSensitive(true);
242    userPassword.addLongIdentifier("user-password");
243    parser.addArgument(userPassword);
244
245    userPasswordFile = new FileArgument(null, "userPasswordFile", false, 1,
246         null,
247         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_USER_PW_FILE.get(
248              authenticationID.getIdentifierString()),
249         true, true, true, false);
250    userPasswordFile.addLongIdentifier("user-password-file");
251    parser.addArgument(userPasswordFile);
252
253    promptForUserPassword = new BooleanArgument(null, "promptForUserPassword",
254         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_PROMPT_FOR_USER_PW.get(
255              authenticationID.getIdentifierString()));
256    promptForUserPassword.addLongIdentifier("prompt-for-user-password");
257    parser.addArgument(promptForUserPassword);
258
259
260    // At most one of the userPassword, userPasswordFile, and
261    // promptForUserPassword arguments must be present.
262    parser.addExclusiveArgumentSet(userPassword, userPasswordFile,
263         promptForUserPassword);
264
265    // If any of the userPassword, userPasswordFile, or promptForUserPassword
266    // arguments is present, then the authenticationID argument must also be
267    // present.
268    parser.addDependentArgumentSet(userPassword, authenticationID);
269    parser.addDependentArgumentSet(userPasswordFile, authenticationID);
270    parser.addDependentArgumentSet(promptForUserPassword, authenticationID);
271  }
272
273
274
275  /**
276   * {@inheritDoc}
277   */
278  @Override()
279  public void doExtendedNonLDAPArgumentValidation()
280         throws ArgumentException
281  {
282    // If the deregister argument was not provided, then the otp argument must
283    // have been given.
284    if ((! deregister.isPresent()) && (! otp.isPresent()))
285    {
286      throw new ArgumentException(
287           ERR_REGISTER_YUBIKEY_OTP_DEVICE_NO_OTP_TO_REGISTER.get(
288                otp.getIdentifierString()));
289    }
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   * {@inheritDoc}
374   */
375  @Override()
376  public ResultCode doToolProcessing()
377  {
378    // Establish a connection to the Directory Server.
379    final LDAPConnection conn;
380    try
381    {
382      conn = getConnection();
383    }
384    catch (final LDAPException le)
385    {
386      Debug.debugException(le);
387      wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
388           ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_CONNECT.get(
389                StaticUtils.getExceptionMessage(le)));
390      return le.getResultCode();
391    }
392
393    try
394    {
395      // Get the authentication ID and static password to include in the
396      // request.
397      final String authID = authenticationID.getValue();
398
399      final byte[] staticPassword;
400      if (userPassword.isPresent())
401      {
402        staticPassword = StaticUtils.getBytes(userPassword.getValue());
403      }
404      else if (userPasswordFile.isPresent())
405      {
406        BufferedReader reader = null;
407        try
408        {
409          reader =
410               new BufferedReader(new FileReader(userPasswordFile.getValue()));
411          staticPassword = StaticUtils.getBytes(reader.readLine());
412        }
413        catch (final Exception e)
414        {
415          Debug.debugException(e);
416          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
417               ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get(
418                    StaticUtils.getExceptionMessage(e)));
419          return ResultCode.LOCAL_ERROR;
420        }
421        finally
422        {
423          if (reader != null)
424          {
425            try
426            {
427              reader.close();
428            }
429            catch (final Exception e)
430            {
431              Debug.debugException(e);
432            }
433          }
434        }
435      }
436      else if (promptForUserPassword.isPresent())
437      {
438        try
439        {
440          getOut().print(INFO_REGISTER_YUBIKEY_OTP_DEVICE_ENTER_PW.get(authID));
441          staticPassword = PasswordReader.readPassword();
442        }
443        catch (final Exception e)
444        {
445          Debug.debugException(e);
446          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
447               ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get(
448                    StaticUtils.getExceptionMessage(e)));
449          return ResultCode.LOCAL_ERROR;
450        }
451      }
452      else
453      {
454        staticPassword = null;
455      }
456
457
458      // Construct and process the appropriate register or deregister request.
459      if (deregister.isPresent())
460      {
461        final DeregisterYubiKeyOTPDeviceExtendedRequest r =
462             new DeregisterYubiKeyOTPDeviceExtendedRequest(authID,
463                  staticPassword, otp.getValue());
464
465        ExtendedResult deregisterResult;
466        try
467        {
468          deregisterResult = conn.processExtendedOperation(r);
469        }
470        catch (final LDAPException le)
471        {
472          deregisterResult = new ExtendedResult(le);
473        }
474
475        if (deregisterResult.getResultCode() == ResultCode.SUCCESS)
476        {
477          if (otp.isPresent())
478          {
479            wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
480                 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ONE.get(
481                      authID));
482          }
483          else
484          {
485            wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
486                 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ALL.get(
487                      authID));
488          }
489          return ResultCode.SUCCESS;
490        }
491        else
492        {
493          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
494               ERR_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_FAILED.get(authID,
495                    String.valueOf(deregisterResult)));
496          return deregisterResult.getResultCode();
497        }
498      }
499      else
500      {
501        final RegisterYubiKeyOTPDeviceExtendedRequest r =
502             new RegisterYubiKeyOTPDeviceExtendedRequest(authID, staticPassword,
503                  otp.getValue());
504
505        ExtendedResult registerResult;
506        try
507        {
508          registerResult = conn.processExtendedOperation(r);
509        }
510        catch (final LDAPException le)
511        {
512          registerResult = new ExtendedResult(le);
513        }
514
515        if (registerResult.getResultCode() == ResultCode.SUCCESS)
516        {
517          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
518               INFO_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_SUCCESS.get(authID));
519          return ResultCode.SUCCESS;
520        }
521        else
522        {
523          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
524               ERR_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_FAILED.get(authID,
525                    String.valueOf(registerResult)));
526          return registerResult.getResultCode();
527        }
528      }
529    }
530    finally
531    {
532      conn.close();
533    }
534  }
535
536
537
538  /**
539   * {@inheritDoc}
540   */
541  @Override()
542  public LinkedHashMap<String[],String> getExampleUsages()
543  {
544    final LinkedHashMap<String[],String> exampleMap =
545         new LinkedHashMap<String[],String>(2);
546
547    String[] args =
548    {
549      "--hostname", "server.example.com",
550      "--port", "389",
551      "--bindDN", "uid=admin,dc=example,dc=com",
552      "--bindPassword", "adminPassword",
553      "--authenticationID", "u:test.user",
554      "--userPassword", "testUserPassword",
555      "--otp", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr"
556    };
557    exampleMap.put(args,
558         INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_REGISTER.get());
559
560    args = new String[]
561    {
562      "--hostname", "server.example.com",
563      "--port", "389",
564      "--bindDN", "uid=admin,dc=example,dc=com",
565      "--bindPassword", "adminPassword",
566      "--deregister",
567      "--authenticationID", "dn:uid=test.user,ou=People,dc=example,dc=com"
568    };
569    exampleMap.put(args,
570         INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_DEREGISTER.get());
571
572    return exampleMap;
573  }
574}