001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 */
016package org.opends.admin.ads.util;
017
018import static org.forgerock.opendj.config.client.ldap.LDAPManagementContext.*;
019import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
020import static org.forgerock.opendj.ldap.requests.Requests.*;
021import static org.forgerock.util.time.Duration.*;
022import static org.opends.admin.ads.util.ConnectionUtils.*;
023import static org.opends.admin.ads.util.PreferredConnection.Type.*;
024import static org.opends.messages.AdminToolMessages.*;
025
026import java.io.Closeable;
027import java.net.URI;
028import java.net.URISyntaxException;
029import java.security.GeneralSecurityException;
030import java.security.NoSuchAlgorithmException;
031import java.util.concurrent.TimeUnit;
032
033import javax.naming.NamingException;
034import javax.naming.NoPermissionException;
035import javax.naming.ldap.InitialLdapContext;
036import javax.net.ssl.KeyManager;
037import javax.net.ssl.SSLContext;
038import javax.net.ssl.TrustManager;
039
040import com.forgerock.opendj.cli.ConnectionFactoryProvider;
041import org.forgerock.opendj.config.LDAPProfile;
042import org.forgerock.opendj.ldap.Connection;
043import org.forgerock.opendj.ldap.LDAPConnectionFactory;
044import org.forgerock.opendj.ldap.LdapException;
045import org.forgerock.opendj.ldap.SSLContextBuilder;
046import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
047import org.forgerock.opendj.server.config.client.RootCfgClient;
048import org.forgerock.util.Options;
049import org.opends.admin.ads.util.PreferredConnection.Type;
050import org.opends.server.types.HostPort;
051import org.opends.server.util.StaticUtils;
052
053/**
054 * Wraps a connection to a directory, either relying on JNDI or relying on OpenDJ Connection.
055 * <p>
056 * You can either:
057 * <ul>
058 *  <li>call {@code getLdapContext()} method to obtain an {@code InitialLdapContext} for JNDI.</li>
059 *  <li>or call the {@code getConnection()} method to obtain a {@code Connection} object.</li>
060 * </ul>
061 */
062public class ConnectionWrapper implements Closeable
063{
064  private final LDAPConnectionFactory connectionFactory;
065  private final Connection connection;
066  private final InitialLdapContext ldapContext;
067  private final HostPort hostPort;
068  private final int connectTimeout;
069  private final TrustManager trustManager;
070  private final KeyManager keyManager;
071
072  /**
073   * Creates a connection wrapper.
074   *
075   * @param ldapUrl
076   *          the ldap URL containing the host name and port number to connect to
077   * @param connectionType
078   *          the type of connection (LDAP, LDAPS, START_TLS)
079   * @param bindDn
080   *          the bind DN
081   * @param bindPwd
082   *          the bind password
083   * @param connectTimeout
084   *          connect timeout to use for the connection
085   * @param trustManager
086   *          trust manager to use for a secure connection
087   * @throws NamingException
088   *           If an error occurs
089   */
090  public ConnectionWrapper(String ldapUrl, Type connectionType, String bindDn, String bindPwd, int connectTimeout,
091      ApplicationTrustManager trustManager) throws NamingException
092  {
093    this(toHostPort(ldapUrl), connectionType, bindDn, bindPwd, connectTimeout, trustManager);
094  }
095
096  private static HostPort toHostPort(String ldapUrl) throws NamingException
097  {
098    try
099    {
100      URI uri = new URI(ldapUrl);
101      return new HostPort(uri.getHost(), uri.getPort());
102    }
103    catch (URISyntaxException e)
104    {
105      throw new NamingException(e.getLocalizedMessage() + ". LDAP URL was: \"" + ldapUrl + "\"");
106    }
107  }
108
109  /**
110   * Creates a connection wrapper.
111   *
112   * @param hostPort
113   *          the host name and port number to connect to
114   * @param connectionType
115   *          the type of connection (LDAP, LDAPS, START_TLS)
116   * @param bindDn
117   *          the bind DN
118   * @param bindPwd
119   *          the bind password
120   * @param connectTimeout
121   *          connect timeout to use for the connection
122   * @param trustManager
123   *          trust manager to use for a secure connection
124   * @throws NamingException
125   *           If an error occurs
126   */
127  public ConnectionWrapper(HostPort hostPort, Type connectionType, String bindDn, String bindPwd, int connectTimeout,
128      TrustManager trustManager) throws NamingException
129  {
130    this(hostPort, connectionType, bindDn, bindPwd, connectTimeout, trustManager, null);
131  }
132
133  /**
134   * Creates a connection wrapper.
135   *
136   * @param hostPort
137   *          the host name and port number to connect to
138   * @param connectionType
139   *          the type of connection (LDAP, LDAPS, START_TLS)
140   * @param bindDn
141   *          the bind DN
142   * @param bindPwd
143   *          the bind password
144   * @param connectTimeout
145   *          connect timeout to use for the connection
146   * @param trustManager
147   *          trust manager to use for a secure connection
148   * @param keyManager
149   *          key manager to use for a secure connection
150   * @throws NamingException
151   *           If an error occurs
152   */
153  public ConnectionWrapper(HostPort hostPort, PreferredConnection.Type connectionType, String bindDn, String bindPwd,
154      int connectTimeout, TrustManager trustManager, KeyManager keyManager) throws NamingException
155  {
156    this.hostPort = hostPort;
157    this.connectTimeout = connectTimeout;
158    this.trustManager = trustManager;
159    this.keyManager = keyManager;
160
161    final Options options = toOptions(connectionType, bindDn, bindPwd, connectTimeout, trustManager, keyManager);
162    ldapContext = createAdministrativeContext(options, bindDn, bindPwd);
163    connectionFactory = new LDAPConnectionFactory(hostPort.getHost(), hostPort.getPort(), options);
164    connection = buildConnection();
165  }
166
167  private static Options toOptions(Type connectionType, String bindDn, String bindPwd, long connectTimeout,
168      TrustManager trustManager, KeyManager keyManager) throws NamingException
169  {
170    final boolean isStartTls = START_TLS.equals(connectionType);
171    final boolean isLdaps = LDAPS.equals(connectionType);
172
173    Options options = Options.defaultOptions()
174        .set(CONNECT_TIMEOUT, duration(connectTimeout, TimeUnit.MILLISECONDS));
175    if (isLdaps || isStartTls)
176    {
177      try {
178        options.set(SSL_CONTEXT, getSSLContext(trustManager, keyManager))
179                .set(SSL_USE_STARTTLS, isStartTls)
180                .set(SSL_ENABLED_PROTOCOLS, ConnectionFactoryProvider.getDefaultProtocols());
181      } catch (NoSuchAlgorithmException e) {
182          throw new NamingException("Unable to perform SSL initialization:" + e.getMessage());
183      }
184    }
185    SimpleBindRequest request = bindDn != null && bindPwd != null
186        ? newSimpleBindRequest(bindDn, bindPwd.toCharArray())
187        : newSimpleBindRequest(); // anonymous bind
188    options.set(AUTHN_BIND_REQUEST, request);
189    return options;
190  }
191
192  private static SSLContext getSSLContext(TrustManager trustManager, KeyManager keyManager) throws NamingException
193  {
194    try
195    {
196      return new SSLContextBuilder()
197          .setTrustManager(trustManager != null ? trustManager : new BlindTrustManager())
198          .setKeyManager(keyManager).getSSLContext();
199    }
200    catch (GeneralSecurityException e)
201    {
202      throw new NamingException("Unable to perform SSL initialization:" + e.getMessage());
203    }
204  }
205
206  private InitialLdapContext createAdministrativeContext(Options options, String bindDn, String bindPwd)
207      throws NamingException
208  {
209    final InitialLdapContext ctx = createAdministrativeContext0(options, bindDn, bindPwd);
210    if (!connectedAsAdministrativeUser(ctx))
211    {
212      throw new NoPermissionException(ERR_NOT_ADMINISTRATIVE_USER.get().toString());
213    }
214    return ctx;
215  }
216
217  private InitialLdapContext createAdministrativeContext0(Options options, String bindDn, String bindPwd)
218      throws NamingException
219  {
220    SSLContext sslContext = options.get(SSL_CONTEXT);
221    boolean useSSL = sslContext != null;
222    boolean useStartTLS = options.get(SSL_USE_STARTTLS);
223    final String ldapUrl = getLDAPUrl(getHostPort(), useSSL);
224    if (useSSL)
225    {
226      return createLdapsContext(ldapUrl, bindDn, bindPwd, connectTimeout, null, trustManager, keyManager);
227    }
228    else if (useStartTLS)
229    {
230      return createStartTLSContext(ldapUrl, bindDn, bindPwd, connectTimeout, null, trustManager, keyManager, null);
231    }
232    else
233    {
234      return createLdapContext(ldapUrl, bindDn, bindPwd, connectTimeout, null);
235    }
236  }
237
238  private Connection buildConnection() throws NamingException
239  {
240    try
241    {
242      return connectionFactory.getConnection();
243    }
244    catch (LdapException e)
245    {
246      throw new NamingException("Unable to get a connection from connection factory:" + e.getMessage());
247    }
248  }
249
250  /**
251   * Returns the connection.
252   *
253   * @return the connection
254   */
255  public Connection getConnection()
256  {
257    return connection;
258  }
259
260  /**
261   * Returns the ldap context (JNDI).
262   *
263   * @return the ldap context
264   */
265  public InitialLdapContext getLdapContext()
266  {
267    return ldapContext;
268  }
269
270  /**
271   * Returns the host name and port number of this connection.
272   *
273   * @return the hostPort of this connection
274   */
275  public HostPort getHostPort()
276  {
277    return hostPort;
278  }
279
280  /**
281   * Returns the root configuration client by using the inrnal Connection.
282   *
283   * @return the root configuration client
284   */
285  public RootCfgClient getRootConfiguration()
286  {
287    return newManagementContext(getConnection(), LDAPProfile.getInstance()).getRootConfiguration();
288  }
289
290  @Override
291  public void close()
292  {
293    StaticUtils.close(connectionFactory, connection);
294    StaticUtils.close(ldapContext);
295  }
296}