001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2007-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.admin.client.cli;
028
029import static com.forgerock.opendj.cli.ArgumentConstants.*;
030import static com.forgerock.opendj.cli.CliMessages.*;
031import static com.forgerock.opendj.cli.ReturnCode.*;
032import static com.forgerock.opendj.cli.Utils.*;
033
034import java.io.File;
035import java.io.FileInputStream;
036import java.io.IOException;
037import java.net.InetAddress;
038import java.security.KeyStore;
039import java.security.KeyStoreException;
040import java.security.NoSuchAlgorithmException;
041import java.security.cert.CertificateException;
042import java.util.ArrayList;
043import java.util.LinkedHashSet;
044import java.util.List;
045import java.util.Set;
046
047import org.forgerock.i18n.LocalizableMessage;
048import org.forgerock.i18n.LocalizableMessageBuilder;
049import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1;
050import org.forgerock.i18n.slf4j.LocalizedLogger;
051import org.forgerock.opendj.config.server.ConfigException;
052import org.opends.admin.ads.util.ApplicationTrustManager;
053import org.opends.server.admin.AdministrationConnector;
054import org.opends.server.admin.server.ServerManagementContext;
055import org.opends.server.admin.std.server.AdministrationConnectorCfg;
056import org.opends.server.admin.std.server.FileBasedTrustManagerProviderCfg;
057import org.opends.server.admin.std.server.RootCfg;
058import org.opends.server.admin.std.server.TrustManagerProviderCfg;
059import org.opends.server.core.DirectoryServer;
060
061import com.forgerock.opendj.cli.Argument;
062import com.forgerock.opendj.cli.ArgumentException;
063import com.forgerock.opendj.cli.BooleanArgument;
064import com.forgerock.opendj.cli.CliConstants;
065import com.forgerock.opendj.cli.CommonArguments;
066import com.forgerock.opendj.cli.FileBasedArgument;
067import com.forgerock.opendj.cli.IntegerArgument;
068import com.forgerock.opendj.cli.StringArgument;
069
070/**
071 * This is a commodity class that can be used to check the arguments required to
072 * establish a secure connection in the command line. It can be used to generate
073 * an ApplicationTrustManager object based on the options provided by the user
074 * in the command line.
075 */
076public final class SecureConnectionCliArgs
077{
078  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
079
080  /** The 'hostName' global argument. */
081  public StringArgument hostNameArg;
082  /** The 'port' global argument. */
083  public IntegerArgument portArg;
084  /** The 'bindDN' global argument. */
085  public StringArgument bindDnArg;
086  /** The 'adminUID' global argument. */
087  public StringArgument adminUidArg;
088  /** The 'bindPasswordFile' global argument. */
089  public FileBasedArgument bindPasswordFileArg;
090  /** The 'bindPassword' global argument. */
091  public StringArgument bindPasswordArg;
092  /** The 'trustAllArg' global argument. */
093  public BooleanArgument trustAllArg;
094  /** The 'trustStore' global argument. */
095  public StringArgument trustStorePathArg;
096  /** The 'trustStorePassword' global argument. */
097  public StringArgument trustStorePasswordArg;
098  /** The 'trustStorePasswordFile' global argument. */
099  public FileBasedArgument trustStorePasswordFileArg;
100  /** The 'keyStore' global argument. */
101  public StringArgument keyStorePathArg;
102  /** The 'keyStorePassword' global argument. */
103  public StringArgument keyStorePasswordArg;
104  /** The 'keyStorePasswordFile' global argument. */
105  public FileBasedArgument keyStorePasswordFileArg;
106  /** The 'certNicknameArg' global argument. */
107  public StringArgument certNicknameArg;
108  /** The 'useSSLArg' global argument. */
109  public BooleanArgument useSSLArg;
110  /** The 'useStartTLSArg' global argument. */
111  public BooleanArgument useStartTLSArg;
112  /** Argument indicating a SASL option. */
113  public StringArgument saslOptionArg;
114  /** Argument to specify the connection timeout. */
115  public IntegerArgument connectTimeoutArg;
116
117  /** Private container for global arguments. */
118  private Set<Argument> argList;
119
120  /** The trust manager. */
121  private ApplicationTrustManager trustManager;
122
123  private boolean configurationInitialized;
124
125  /** Defines if the CLI always use the SSL connection type. */
126  private boolean alwaysSSL;
127
128  /**
129   * Creates a new instance of secure arguments.
130   *
131   * @param alwaysSSL
132   *          If true, always use the SSL connection type. In this case, the
133   *          arguments useSSL and startTLS are not present.
134   */
135  public SecureConnectionCliArgs(boolean alwaysSSL)
136  {
137    this.alwaysSSL = alwaysSSL;
138  }
139
140  /**
141   * Indicates whether or not any of the arguments are present.
142   *
143   * @return boolean where true indicates that at least one of the arguments is
144   *         present
145   */
146  public boolean argumentsPresent()
147  {
148    if (argList != null)
149    {
150      for (Argument arg : argList)
151      {
152        if (arg.isPresent())
153        {
154          return true;
155        }
156      }
157    }
158    return false;
159  }
160
161  /**
162   * Get the admin UID which has to be used for the command.
163   *
164   * @return The admin UID specified by the command line argument, or the
165   *         default value, if not specified.
166   */
167  public String getAdministratorUID()
168  {
169    if (adminUidArg.isPresent())
170    {
171      return adminUidArg.getValue();
172    }
173    return adminUidArg.getDefaultValue();
174  }
175
176  /**
177   * Tells whether this parser uses the Administrator UID (instead of the bind
178   * DN) or not.
179   *
180   * @return {@code true} if this parser uses the Administrator UID and
181   *         {@code false} otherwise.
182   */
183  public boolean useAdminUID()
184  {
185    return !adminUidArg.isHidden();
186  }
187
188  /**
189   * Get the bindDN which has to be used for the command.
190   *
191   * @return The bindDN specified by the command line argument, or the default
192   *         value, if not specified.
193   */
194  public String getBindDN()
195  {
196    if (bindDnArg.isPresent())
197    {
198      return bindDnArg.getValue();
199    }
200    return bindDnArg.getDefaultValue();
201  }
202
203  /**
204   * Initialize Global option.
205   *
206   * @throws ArgumentException
207   *           If there is a problem with any of the parameters used to create
208   *           this argument.
209   * @return a ArrayList with the options created.
210   */
211  public Set<Argument> createGlobalArguments() throws ArgumentException
212  {
213    argList = new LinkedHashSet<>();
214
215    useSSLArg = CommonArguments.getUseSSL();
216    if (!alwaysSSL)
217    {
218      argList.add(useSSLArg);
219    }
220    else
221    {
222      // simulate that the useSSL arg has been given in the CLI
223      useSSLArg.setPresent(true);
224    }
225
226    useStartTLSArg = CommonArguments.getStartTLS();
227    if (!alwaysSSL)
228    {
229      argList.add(useStartTLSArg);
230    }
231
232    String defaultHostName;
233    try
234    {
235      defaultHostName = InetAddress.getLocalHost().getHostName();
236    }
237    catch (Exception e)
238    {
239      defaultHostName = "Unknown (" + e + ")";
240    }
241    hostNameArg = CommonArguments.getHostName(defaultHostName);
242    argList.add(hostNameArg);
243
244    portArg = CommonArguments.getPort(AdministrationConnector.DEFAULT_ADMINISTRATION_CONNECTOR_PORT,
245                                      alwaysSSL ? INFO_DESCRIPTION_ADMIN_PORT.get() : INFO_DESCRIPTION_PORT.get());
246    argList.add(portArg);
247
248    bindDnArg = CommonArguments.getBindDN(CliConstants.DEFAULT_ROOT_USER_DN);
249    argList.add(bindDnArg);
250
251    // It is up to the classes that required admin UID to make this argument
252    // visible and add it.
253    adminUidArg = new StringArgument("adminUID", 'I', OPTION_LONG_ADMIN_UID, false, false, true,
254                                     INFO_ADMINUID_PLACEHOLDER.get(), CliConstants.GLOBAL_ADMIN_UID,
255                                     null, INFO_DESCRIPTION_ADMIN_UID.get());
256    adminUidArg.setPropertyName(OPTION_LONG_ADMIN_UID);
257    adminUidArg.setHidden(true);
258
259    bindPasswordArg = CommonArguments.getBindPassword();
260    argList.add(bindPasswordArg);
261
262    bindPasswordFileArg = CommonArguments.getBindPasswordFile();
263    argList.add(bindPasswordFileArg);
264
265    saslOptionArg = CommonArguments.getSASL();
266    argList.add(saslOptionArg);
267
268    trustAllArg = CommonArguments.getTrustAll();
269    argList.add(trustAllArg);
270
271    trustStorePathArg = CommonArguments.getTrustStorePath();
272    argList.add(trustStorePathArg);
273
274    trustStorePasswordArg = CommonArguments.getTrustStorePassword();
275    argList.add(trustStorePasswordArg);
276
277    trustStorePasswordFileArg = CommonArguments.getTrustStorePasswordFile();
278    argList.add(trustStorePasswordFileArg);
279
280    keyStorePathArg = CommonArguments.getKeyStorePath();
281    argList.add(keyStorePathArg);
282
283    keyStorePasswordArg = CommonArguments.getKeyStorePassword();
284    argList.add(keyStorePasswordArg);
285
286    keyStorePasswordFileArg = CommonArguments.getKeyStorePasswordFile();
287    argList.add(keyStorePasswordFileArg);
288
289    certNicknameArg = CommonArguments.getCertNickName();
290    argList.add(certNicknameArg);
291
292    connectTimeoutArg = CommonArguments.getConnectTimeOut();
293    connectTimeoutArg.setHidden(false);
294    argList.add(connectTimeoutArg);
295
296    return argList;
297  }
298
299  /**
300   * Get the host name which has to be used for the command.
301   *
302   * @return The host name specified by the command line argument, or the
303   *         default value, if not specified.
304   */
305  public String getHostName()
306  {
307    if (hostNameArg.isPresent())
308    {
309      return hostNameArg.getValue();
310    }
311    return hostNameArg.getDefaultValue();
312  }
313
314  /**
315   * Get the port which has to be used for the command.
316   *
317   * @return The port specified by the command line argument, or the default
318   *         value, if not specified.
319   */
320  public String getPort()
321  {
322    if (portArg.isPresent())
323    {
324      return portArg.getValue();
325    }
326    return portArg.getDefaultValue();
327  }
328
329  /**
330   * Indication if provided global options are validate.
331   *
332   * @param buf
333   *          the LocalizableMessageBuilder to write the error messages.
334   * @return return code.
335   */
336  public int validateGlobalOptions(LocalizableMessageBuilder buf)
337  {
338    List<LocalizableMessage> errors = new ArrayList<>();
339
340    addIfArgsAreConflicting(errors, bindPasswordArg, bindPasswordFileArg);
341
342    // Couldn't have at the same time trustAll and trustStore related args
343    addIfArgsAreConflicting(errors, trustAllArg, trustStorePathArg);
344    addIfArgsAreConflicting(errors, trustAllArg, trustStorePasswordArg);
345    addIfArgsAreConflicting(errors, trustAllArg, trustStorePasswordFileArg);
346
347    addIfArgsAreConflicting(errors, trustStorePasswordArg, trustStorePasswordFileArg);
348
349    checkIfPathArgumentIsReadable(errors, trustStorePathArg, ERR_CANNOT_READ_TRUSTSTORE);
350    checkIfPathArgumentIsReadable(errors, keyStorePathArg, ERR_CANNOT_READ_KEYSTORE);
351
352    addIfArgsAreConflicting(errors, useStartTLSArg, useSSLArg);
353
354    if (!errors.isEmpty())
355    {
356      for (LocalizableMessage error : errors)
357      {
358        if (buf.length() > 0)
359        {
360          buf.append(LINE_SEPARATOR);
361        }
362        buf.append(error);
363      }
364      return CONFLICTING_ARGS.get();
365    }
366
367    return SUCCESS.get();
368  }
369
370  private void addIfArgsAreConflicting(List<LocalizableMessage> errors, Argument arg1, Argument arg2)
371  {
372    if (arg1.isPresent() && arg2.isPresent())
373    {
374      errors.add(ERR_TOOL_CONFLICTING_ARGS.get(arg1.getLongIdentifier(), arg2.getLongIdentifier()));
375    }
376  }
377
378  private void checkIfPathArgumentIsReadable(List<LocalizableMessage> errors, StringArgument pathArg, Arg1<Object> msg)
379  {
380    if (pathArg.isPresent() && !canRead(pathArg.getValue()))
381    {
382      errors.add(msg.get(pathArg.getValue()));
383    }
384  }
385
386  /**
387   * Indicate if the SSL mode is required.
388   *
389   * @return True if SSL mode is required
390   */
391  public boolean useSSL()
392  {
393    return useSSLArg.isPresent() || alwaysSSL();
394  }
395
396  /**
397   * Indicate if the startTLS mode is required.
398   *
399   * @return True if startTLS mode is required
400   */
401  public boolean useStartTLS()
402  {
403    return useStartTLSArg.isPresent();
404  }
405
406  /**
407   * Indicate if the SSL mode is always used.
408   *
409   * @return True if SSL mode is always used.
410   */
411  public boolean alwaysSSL()
412  {
413    return alwaysSSL;
414  }
415
416  /**
417   * Handle TrustStore.
418   *
419   * @return The trustStore manager to be used for the command.
420   */
421  public ApplicationTrustManager getTrustManager()
422  {
423    if (trustManager == null)
424    {
425      KeyStore truststore = null;
426      if (trustAllArg.isPresent())
427      {
428        // Running a null TrustManager  will force createLdapsContext and
429        // createStartTLSContext to use a bindTrustManager.
430        return null;
431      }
432      else if (trustStorePathArg.isPresent())
433      {
434        try (final FileInputStream fos = new FileInputStream(trustStorePathArg.getValue()))
435        {
436          String trustStorePasswordStringValue = null;
437          if (trustStorePasswordArg.isPresent())
438          {
439            trustStorePasswordStringValue = trustStorePasswordArg.getValue();
440          }
441          else if (trustStorePasswordFileArg.isPresent())
442          {
443            trustStorePasswordStringValue = trustStorePasswordFileArg.getValue();
444          }
445
446          if (trustStorePasswordStringValue != null)
447          {
448            trustStorePasswordStringValue = System.getProperty("javax.net.ssl.trustStorePassword");
449          }
450
451          char[] trustStorePasswordValue = null;
452          if (trustStorePasswordStringValue != null)
453          {
454            trustStorePasswordValue = trustStorePasswordStringValue.toCharArray();
455          }
456
457          truststore = KeyStore.getInstance(KeyStore.getDefaultType());
458          truststore.load(fos, trustStorePasswordValue);
459        }
460        catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e)
461        {
462          // Nothing to do: if this occurs we will systematically refuse the
463          // certificates.  Maybe we should avoid this and be strict, but we
464          // are in a best effort mode.
465          logger.warn(LocalizableMessage.raw("Error with the truststore"), e);
466        }
467      }
468      trustManager = new ApplicationTrustManager(truststore);
469    }
470    return trustManager;
471  }
472
473  /**
474   * Returns {@code true} if we can read on the provided path and {@code false}
475   * otherwise.
476   *
477   * @param path
478   *          the path.
479   * @return {@code true} if we can read on the provided path and {@code false}
480   *         otherwise.
481   */
482  private boolean canRead(String path)
483  {
484    final File file = new File(path);
485    return file.exists() && file.canRead();
486  }
487
488  /**
489   * Returns the absolute path of the trust store file that appears on the
490   * config. Returns {@code null} if the trust store is not defined or it does
491   * not exist.
492   *
493   * @return the absolute path of the trust store file that appears on the
494   *         config.
495   * @throws ConfigException
496   *           if there is an error reading the configuration.
497   */
498  public String getTruststoreFileFromConfig() throws ConfigException
499  {
500    String truststoreFileAbsolute = null;
501    TrustManagerProviderCfg trustManagerCfg = null;
502    AdministrationConnectorCfg administrationConnectorCfg = null;
503
504    boolean couldInitializeConfig = configurationInitialized;
505    // Initialization for admin framework
506    if (!configurationInitialized)
507    {
508      couldInitializeConfig = initializeConfiguration();
509    }
510    if (couldInitializeConfig)
511    {
512      // Get the Directory Server configuration handler and use it.
513      RootCfg root = ServerManagementContext.getInstance().getRootConfiguration();
514      administrationConnectorCfg = root.getAdministrationConnector();
515
516      String trustManagerStr = administrationConnectorCfg.getTrustManagerProvider();
517      trustManagerCfg = root.getTrustManagerProvider(trustManagerStr);
518      if (trustManagerCfg instanceof FileBasedTrustManagerProviderCfg)
519      {
520        FileBasedTrustManagerProviderCfg fileBasedTrustManagerCfg = (FileBasedTrustManagerProviderCfg) trustManagerCfg;
521        String truststoreFile = fileBasedTrustManagerCfg.getTrustStoreFile();
522        // Check the file
523        if (truststoreFile.startsWith(File.separator))
524        {
525          truststoreFileAbsolute = truststoreFile;
526        }
527        else
528        {
529          truststoreFileAbsolute = DirectoryServer.getInstanceRoot() + File.separator + truststoreFile;
530        }
531        File f = new File(truststoreFileAbsolute);
532        if (!f.exists() || !f.canRead() || f.isDirectory())
533        {
534          truststoreFileAbsolute = null;
535        }
536        else
537        {
538          // Try to get the canonical path.
539          try
540          {
541            truststoreFileAbsolute = f.getCanonicalPath();
542          }
543          catch (Throwable t)
544          {
545            // We can ignore this error.
546          }
547        }
548      }
549    }
550    return truststoreFileAbsolute;
551  }
552
553  /**
554   * Returns the admin port from the configuration.
555   *
556   * @return the admin port from the configuration.
557   * @throws ConfigException
558   *           if an error occurs reading the configuration.
559   */
560  public int getAdminPortFromConfig() throws ConfigException
561  {
562    // Initialization for admin framework
563    boolean couldInitializeConfiguration = configurationInitialized;
564    if (!configurationInitialized)
565    {
566      couldInitializeConfiguration = initializeConfiguration();
567    }
568    if (couldInitializeConfiguration)
569    {
570      RootCfg root = ServerManagementContext.getInstance().getRootConfiguration();
571      return root.getAdministrationConnector().getListenPort();
572    }
573    else
574    {
575      return AdministrationConnector.DEFAULT_ADMINISTRATION_CONNECTOR_PORT;
576    }
577  }
578
579  private boolean initializeConfiguration()
580  {
581    // check if the initialization is required
582    try
583    {
584      ServerManagementContext.getInstance().getRootConfiguration().getAdministrationConnector();
585    }
586    catch (java.lang.Throwable th)
587    {
588      try
589      {
590        DirectoryServer.bootstrapClient();
591        DirectoryServer.initializeJMX();
592        DirectoryServer.getInstance().initializeConfiguration();
593      }
594      catch (Exception ex)
595      {
596        // do nothing
597        return false;
598      }
599    }
600    configurationInitialized = true;
601    return true;
602  }
603
604  /**
605   * Returns the port to be used according to the configuration and the
606   * arguments provided by the user. This method should be called after the
607   * arguments have been parsed.
608   *
609   * @return the port to be used according to the configuration and the
610   *         arguments provided by the user.
611   */
612  public int getPortFromConfig()
613  {
614    int portNumber;
615    if (alwaysSSL())
616    {
617      portNumber = AdministrationConnector.DEFAULT_ADMINISTRATION_CONNECTOR_PORT;
618      // Try to get the port from the config file
619      try
620      {
621        portNumber = getAdminPortFromConfig();
622      }
623      catch (ConfigException ex)
624      {
625        // Nothing to do
626      }
627    }
628    else
629    {
630      portNumber = CliConstants.DEFAULT_SSL_PORT;
631    }
632    return portNumber;
633  }
634
635  /**
636   * Updates the default values of the port and the trust store with what is
637   * read in the configuration.
638   *
639   * @throws ConfigException
640   *           if there is an error reading the configuration.
641   */
642  public void initArgumentsWithConfiguration() throws ConfigException
643  {
644    portArg.setDefaultValue(String.valueOf(getPortFromConfig()));
645
646    String truststoreFileAbsolute = getTruststoreFileFromConfig();
647    if (truststoreFileAbsolute != null)
648    {
649      trustStorePathArg.setDefaultValue(truststoreFileAbsolute);
650    }
651  }
652}