mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Jean-Noel Rouvignac
05.21.2013 503283bea9d628b1d118107d62a7c0fd6df61fce
OPENDJ-830 (CR-1505) Implement authentication and authorization for HTTP connection handler

Implemented authentication with a synchronous username search + bind.
To come next: asynchronous username search + bind.


HTTPAuthenticationConfig.java: ADDED

build.xml, ivy.xml:
Added some tests jars + updated grizzly to the newly released 2.3 stable version.

CollectClientConnectionsFilter.java:
Added HTTPAuthenticationConfig instance member to configure the HTTP auth from the JSON file.
Added many methods to handle HTTP auth in a synchronous way. Will have to change this code to run asynchronously for performance.

HTTPClientConnection.java:
Changed a few signatures and types to handle more than Search operations.

HTTPConnectionHandler.java:
Added methods parseJsonConfiguration(), getAuthenticationConfig(), asString() and asBool() to read the config for HTTP Authentication.

SdkConnectionAdapter.java:
Handled several messages being sent through the same connection.
Extracted methods enqueueOperation() from searchAsync().
Implemented bindAsync().

Adapters.java, Converters.java:
Added new conversion methods.
Renamed local variables to correctly use camel case.

ConvertersTestCase.java:
Removed try / catch / empty anti pattern.
2 files added
6 files modified
836 ■■■■■ changed files
opends/build.xml 15 ●●●●● patch | view | raw | blame | history
opends/ivy.xml 9 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java 254 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/HTTPAuthenticationConfig.java 233 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java 22 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/HTTPConnectionHandler.java 80 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java 86 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java 137 ●●●●● patch | view | raw | blame | history
opends/build.xml
@@ -181,6 +181,17 @@
  <fileset id="opendj.runtime.jars" dir="${lib.dir}">
    <include name="**/jar/*.jar" />
    <include name="**/bundle/*.jar" />
    <exclude name="**/assertj-core.jar" />
    <exclude name="**/mockito-core.jar" />
    <exclude name="**/objenesis.jar" />
  </fileset>
  <fileset id="opendj.test.jars" dir="${lib.dir}">
    <include name="**/assertj-core.jar" />
    <include name="**/mockito-core.jar" />
    <include name="**/hamcrest-core.jar" />
    <include name="**/objenesis.jar" />
  </fileset>
  <!-- Prevent ant runtime from being included on classpath during
@@ -1590,6 +1601,7 @@
        destdir="${classes.dir}">
      <classpath>
        <fileset refid="opendj.runtime.jars"/>
        <fileset refid="opendj.test.jars"/>
        <fileset dir="${build.dir}/build-tools">
          <include name="build-tools.jar" />
        </fileset>
@@ -1626,6 +1638,7 @@
    <javac srcdir="${unittest.testng.src.dir}" destdir="${unittest.classes.dir}" excludes="org/opends/server/snmp/**">
      <classpath>
        <fileset refid="opendj.runtime.jars"/>
        <fileset refid="opendj.test.jars"/>
        <fileset dir="${testng.lib.dir}">
          <include name="*.jar" />
@@ -2108,6 +2121,7 @@
        <path refid="emma.lib" />
        <fileset refid="opendj.runtime.jars"/>
        <fileset refid="opendj.test.jars"/>
        <!-- Needed by quicksetup tests -->
        <fileset dir="${build.dir}/build-tools">
@@ -2555,6 +2569,7 @@
           includes="org/opends/server/snmp/**">
      <classpath>
        <fileset refid="opendj.runtime.jars"/>
        <fileset refid="opendj.test.jars"/>
        <fileset dir="${testng.lib.dir}">
          <include name="*.jar" />
opends/ivy.xml
@@ -27,7 +27,7 @@
 ! -->
<!-- Using entities prevent constantly declaring the same versions -->
<!DOCTYPE ivy-module [
    <!ENTITY grizzly.version    "2.3-rc6">
    <!ENTITY grizzly.version    "2.3">
    <!ENTITY opendj.sdk.version "3.0.0-SNAPSHOT">
    <!ENTITY crest.version      "2.0.0-SNAPSHOT">
]>
@@ -36,6 +36,8 @@
    xsi:noNamespaceSchemaLocation="http://incubator.apache.org/ivy/schemas/ivy.xsd">
  <info organisation="org.forgerock" module="opendj"/>
  <dependencies>
    <!-- compile + runtime libs -->
    <dependency org="javax.mail"            name="mail"                     rev="1.4.5" />
    <!-- subsequent versions are not compatible with java 6, so force to use this version only -->
    <dependency org="javax.servlet"         name="javax.servlet-api"        rev="[3.1-b02]" />
@@ -49,6 +51,11 @@
      <exclude module="javax.servlet-api" />
    </dependency>
    <!-- Test libs -->
    <!--dependency org="org.testng"            name="testng"                   rev="6.8.1" /-->
    <dependency org="org.assertj"           name="assertj-core"             rev="1.0.0" />
    <dependency org="org.mockito"           name="mockito-core"             rev="1.9.5" />
    <!-- Force download of the source jars -->
    <!--
    <dependency org="org.codehaus.jackson"  name="jackson-core-asl"         rev="1.9.2"                conf="default->master,sources"/>
opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -26,6 +26,7 @@
 */
package org.opends.server.protocols.http;
import static org.forgerock.opendj.adapter.server2x.Converters.*;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.loggers.ErrorLogger.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
@@ -33,47 +34,76 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.Collection;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
import org.opends.messages.Message;
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
import org.opends.server.api.ClientConnection;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.schema.SchemaConstants;
import org.opends.server.types.AddressMask;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.ByteString;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DisconnectReason;
import org.opends.server.util.Base64;
/**
 * Servlet {@link Filter} that collects information about client connections.
 */
final class CollectClientConnectionsFilter implements Filter
final class CollectClientConnectionsFilter implements javax.servlet.Filter
{
  /** HTTP Header sent by the client with HTTP basic authentication. */
  static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
  /** The tracer object for the debug logger. */
  private static final DebugTracer TRACER = getTracer();
  /** The connection handler that created this servlet filter. */
  private final HTTPConnectionHandler connectionHandler;
  /**
   * Configures how to perform the search for the username prior to
   * authentication.
   */
  private final HTTPAuthenticationConfig authConfig;
  /**
   * Constructs a new instance of this class.
   *
   * @param connectionHandler
   *          the connection handler that accepted this connection
   * @param authenticationConfig
   *          configures how to perform the search for the username prior to
   *          authentication
   */
  public CollectClientConnectionsFilter(HTTPConnectionHandler connectionHandler)
  public CollectClientConnectionsFilter(
      HTTPConnectionHandler connectionHandler,
      HTTPAuthenticationConfig authenticationConfig)
  {
    this.connectionHandler = connectionHandler;
    this.authConfig = authenticationConfig;
  }
  /** {@inheritDoc} */
@@ -100,19 +130,36 @@
        return;
      }
      // TODO JNR handle authentication
      Connection connectionAdapter = new SdkConnectionAdapter(clientConnection);
      Connection connection = new SdkConnectionAdapter(clientConnection);
      // WARNING: This action triggers 3-4 others:
      // Set the connection for use with this request on the HttpServletRequest.
      // It will make Rest2LDAPContextFactory create an
      // AuthenticatedConnectionContext which will in turn ensure Rest2LDAP uses
      // the supplied Connection object
      request.setAttribute(Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION,
          connectionAdapter);
      String[] userPassword = extractUsernamePassword(request);
      if (userPassword != null && userPassword.length == 2)
      {
        AuthenticationInfo authInfo =
            authenticate(userPassword[0], userPassword[1], connection);
        if (authInfo != null)
        {
          clientConnection.setAuthenticationInfo(authInfo);
      // send the request further down the filter chain or pass to servlet
      chain.doFilter(request, response);
          /*
           * WARNING: This action triggers 3-4 others: Set the connection for
           * use with this request on the HttpServletRequest. It will make
           * Rest2LDAPContextFactory create an AuthenticatedConnectionContext
           * which will in turn ensure Rest2LDAP uses the supplied Connection
           * object
           */
          request.setAttribute(
              Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, connection);
          // send the request further down the filter chain or pass to servlet
          chain.doFilter(request, response);
          return;
        }
      }
      // The user could not be authenticated. Send an HTTP Basic authentication
      // challenge if HTTP Basic authentication is enabled.
      sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
    }
    catch (Exception e)
    {
@@ -177,6 +224,185 @@
    return true;
  }
  /**
   * Extracts the username and password from the request using one of the
   * enabled authentication mechanism: HTTP Basic authentication or HTTP Custom
   * headers. If no username and password can be obtained, then send back an
   * HTTP basic authentication challenge if HTTP basic authentication is
   * enabled.
   *
   * @param request
   *          the request where to extract the username and password from
   * @return the array containing the username/password couple if both exist,
   *         null otherwise
   */
  String[] extractUsernamePassword(ServletRequest request)
  {
    HttpServletRequest req = (HttpServletRequest) request;
    // TODO Use session to reduce hits with search + bind?
    // Use proxied authorization control for session.
    if (authConfig.isCustomHeadersAuthenticationSupported())
    {
      final String userName =
          req.getHeader(authConfig.getCustomHeaderUsername());
      final String password =
          req.getHeader(authConfig.getCustomHeaderPassword());
      if (userName != null && password != null)
      {
        return new String[] { userName, password };
      }
    }
    if (authConfig.isBasicAuthenticationSupported())
    {
      String httpBasicAuthHeader = req.getHeader(HTTP_BASIC_AUTH_HEADER);
      if (httpBasicAuthHeader != null)
      {
        String[] userPassword = parseUsernamePassword(httpBasicAuthHeader);
        if (userPassword != null)
        {
          return userPassword;
        }
      }
    }
    return null;
  }
  /**
   * Sends an Unauthorized status code and a challenge for HTTP Basic
   * authentication if HTTP basic authentication is enabled.
   *
   * @param response
   *          where to send the Unauthorized status code.
   */
  void sendUnauthorizedResponseWithHTTPBasicAuthChallenge(
      ServletResponse response)
  {
    HttpServletResponse resp = (HttpServletResponse) response;
    resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    if (authConfig.isBasicAuthenticationSupported())
    {
      resp.setHeader("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\"");
    }
  }
  /**
   * Parses username and password from the authentication header used in HTTP
   * basic authentication.
   *
   * @param authHeader
   *          the authentication header obtained from the request
   * @return an array containing the username at index 0 and the password at
   *         index 1, or null if the header cannot be parsed successfully
   */
  String[] parseUsernamePassword(String authHeader)
  {
    if (authHeader != null
        && (authHeader.startsWith("Basic") || authHeader.startsWith("basic")))
    {
      // We received authentication info
      // Example received header:
      // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
      String base64UserPassword = authHeader.substring("basic".length() + 1);
      try
      {
        // Example usage of base64:
        // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
        String userPassword = new String(Base64.decode(base64UserPassword));
        String[] split = userPassword.split(":");
        if (split.length == 2)
        {
          return split;
        }
      }
      catch (ParseException e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
      }
    }
    return null;
  }
  /**
   * Authenticates the user by doing a search on the user name + bind the
   * returned user entry DN, then return the authentication info if search and
   * bind were successful.
   *
   * @param userName
   *          the user name to authenticate
   * @param password
   *          the password to use with the user
   * @param connection
   *          the connection to use for search and bind
   * @return the {@link AuthenticationInfo} for the supplied credentials, null
   *         if authentication was unsuccessful.
   */
  private AuthenticationInfo authenticate(String userName, String password,
      Connection connection)
  {
    // TODO JNR do the next steps in an async way
    SearchResultEntry resultEntry = searchUniqueEntryDN(userName, connection);
    if (resultEntry != null)
    {
      DN bindDN = resultEntry.getName();
      if (bindDN != null && bind(bindDN.toString(), password, connection))
      {
        return new AuthenticationInfo(to(resultEntry), to(bindDN), ByteString
            .valueOf(password), false);
      }
    }
    return null;
  }
  private SearchResultEntry searchUniqueEntryDN(String userName,
      Connection connection)
  {
    // use configured rights to find the user DN
    final Filter filter =
        Filter.format(authConfig.getSearchFilterTemplate(), userName);
    final SearchRequest searchRequest =
        Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig
            .getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
    try
    {
      return connection.searchSingleEntry(searchRequest);
    }
    catch (ErrorResultException e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
    }
    return null;
  }
  private boolean bind(String bindDN, String password, Connection connection)
  {
    BindRequest bindRequest =
        Requests.newSimpleBindRequest(bindDN, password.getBytes());
    try
    {
      BindResult bindResult = connection.bind(bindRequest);
      return ResultCode.SUCCESS.equals(bindResult.getResultCode());
    }
    catch (ErrorResultException e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
    }
    return false;
  }
  /** {@inheritDoc} */
  @Override
  public void destroy()
opends/src/server/org/opends/server/protocols/http/HTTPAuthenticationConfig.java
New file
@@ -0,0 +1,233 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2013 ForgeRock AS
 */
package org.opends.server.protocols.http;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.SearchScope;
/**
 * Class holding the configuration for HTTP authentication. This is extracted
 * from the JSON config file or the config held in LDAP.
 */
class HTTPAuthenticationConfig
{
  private boolean basicAuthenticationSupported;
  private boolean customHeadersAuthenticationSupported;
  private String customHeaderUsername;
  private String customHeaderPassword;
  private DN searchBaseDN;
  private SearchScope searchScope;
  private String searchFilterTemplate;
  /**
   * Returns whether HTTP basic authentication is supported.
   *
   * @return true if supported, false otherwise
   */
  public boolean isBasicAuthenticationSupported()
  {
    return basicAuthenticationSupported;
  }
  /**
   * Sets whether HTTP basic authentication is supported.
   *
   * @param supported
   *          the supported value
   */
  public void setBasicAuthenticationSupported(boolean supported)
  {
    this.basicAuthenticationSupported = supported;
  }
  /**
   * Returns whether HTTP authentication via custom headers is supported.
   *
   * @return true if supported, false otherwise
   */
  public boolean isCustomHeadersAuthenticationSupported()
  {
    return customHeadersAuthenticationSupported;
  }
  /**
   * Sets whether HTTP authentication via custom headers is supported.
   *
   * @param supported
   *          the supported value
   */
  public void setCustomHeadersAuthenticationSupported(boolean supported)
  {
    this.customHeadersAuthenticationSupported = supported;
  }
  /**
   * Returns the expected HTTP header for the username. This setting is only
   * used when HTTP authentication via custom headers is supported.
   *
   * @return the HTTP header for the username
   */
  public String getCustomHeaderUsername()
  {
    return customHeaderUsername;
  }
  /**
   * Sets the expected HTTP header for the username. This setting only takes
   * effect when HTTP authentication via custom headers is supported.
   *
   * @param customHeaderUsername
   *          the HTTP header for the username
   */
  public void setCustomHeaderUsername(String customHeaderUsername)
  {
    this.customHeaderUsername = customHeaderUsername;
  }
  /**
   * Returns the expected HTTP header for the password. This setting is only
   * used when HTTP authentication via custom headers is supported.
   *
   * @return the HTTP header for the password
   */
  public String getCustomHeaderPassword()
  {
    return customHeaderPassword;
  }
  /**
   * Sets the expected HTTP header for the password. This setting only takes
   * effect when HTTP authentication via custom headers is supported.
   *
   * @param customHeaderPassword
   *          the HTTP header for the password
   */
  public void setCustomHeaderPassword(String customHeaderPassword)
  {
    this.customHeaderPassword = customHeaderPassword;
  }
  /**
   * Returns the base DN to use when searching the entry corresponding to the
   * authenticating user.
   *
   * @return the base DN to use when searching the authenticating user
   */
  public DN getSearchBaseDN()
  {
    return searchBaseDN;
  }
  /**
   * Sets the base DN to use when searching the entry corresponding to the
   * authenticating user.
   *
   * @param searchBaseDN
   *          the base DN to use when searching the authenticating user
   */
  public void setSearchBaseDN(DN searchBaseDN)
  {
    this.searchBaseDN = searchBaseDN;
  }
  /**
   * Returns the search scope to use when searching the entry corresponding to
   * the authenticating user.
   *
   * @return the search scope to use when searching the authenticating user
   */
  public SearchScope getSearchScope()
  {
    return searchScope;
  }
  /**
   * Sets the search scope to use when searching the entry corresponding to the
   * authenticating user.
   *
   * @param searchScope
   *          the search scope to use when searching the authenticating user
   */
  public void setSearchScope(SearchScope searchScope)
  {
    this.searchScope = searchScope;
  }
  /**
   * Returns the search filter template to use when searching the entry
   * corresponding to the authenticating user.
   *
   * @return the search filter template to use when searching the authenticating
   *         user
   */
  public String getSearchFilterTemplate()
  {
    return searchFilterTemplate;
  }
  /**
   * Sets the search filter template to use when searching the entry
   * corresponding to the authenticating user.
   *
   * @param searchFilterTemplate
   *          the search filter template to use when searching the
   *          authenticating user
   */
  public void setSearchFilterTemplate(String searchFilterTemplate)
  {
    this.searchFilterTemplate = searchFilterTemplate;
  }
  /** {@inheritDoc} */
  @Override
  public String toString()
  {
    StringBuilder sb = new StringBuilder();
    sb.append("basicAuth: ");
    if (!basicAuthenticationSupported)
    {
      sb.append("not ");
    }
    sb.append("supported, ");
    sb.append("customHeadersAuth: ");
    if (customHeadersAuthenticationSupported)
    {
      sb.append("usernameHeader=\"").append(customHeaderUsername).append("\",");
      sb.append("passwordHeader=\"").append(customHeaderPassword).append("\"");
    }
    else
    {
      sb.append("not supported, ");
    }
    sb.append("searchBaseDN: \"").append(searchBaseDN).append("\"");
    sb.append("searchScope: \"").append(searchScope).append("\"");
    sb.append("searchFilterTemplate: \"").append(searchFilterTemplate).append(
        "\"");
    return sb.toString();
  }
}
opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
@@ -41,6 +41,7 @@
import javax.servlet.ServletRequest;
import org.forgerock.opendj.ldap.ErrorResultException;
import org.forgerock.opendj.ldap.ResultHandler;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.responses.Result;
import org.opends.messages.Message;
@@ -86,15 +87,22 @@
  {
    final Operation operation;
    final AsynchronousFutureResult<Result, SearchResultHandler> futureResult;
    final AsynchronousFutureResult<Result, ResultHandler<? super Result>>
            futureResult;
    public OperationWithFutureResult(Operation operation,
        AsynchronousFutureResult<Result, SearchResultHandler> futureResult)
        AsynchronousFutureResult<Result, ResultHandler<? super Result>>
        futureResult)
    {
      this.operation = operation;
      this.futureResult = futureResult;
    }
    @Override
    public String toString()
    {
      return operation.toString();
    }
  }
  /** The tracer object for the debug logger. */
@@ -282,7 +290,8 @@
        this.operationsInProgress.get(operation.getMessageID());
    if (op != null)
    {
      op.futureResult.getResultHandler().handleEntry(from(searchEntry));
      ((SearchResultHandler) op.futureResult.getResultHandler())
          .handleEntry(from(searchEntry));
    }
  }
@@ -295,7 +304,8 @@
        this.operationsInProgress.get(operation.getMessageID());
    if (op != null)
    {
      op.futureResult.getResultHandler().handleReference(from(searchReference));
      ((SearchResultHandler) op.futureResult.getResultHandler())
          .handleReference(from(searchReference));
    }
    return connectionValid;
  }
@@ -402,8 +412,8 @@
   *           If an error occurs
   */
  void addOperationInProgress(Operation operation,
      AsynchronousFutureResult<Result, SearchResultHandler> futureResult)
      throws DirectoryException
      AsynchronousFutureResult<Result, ResultHandler<? super Result>>
          futureResult) throws DirectoryException
  {
    synchronized (opsInProgressLock)
    {
opends/src/server/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -52,10 +52,12 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletException;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.resource.CollectionResourceProvider;
@@ -63,6 +65,7 @@
import org.forgerock.json.resource.Resources;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.servlet.HttpServlet;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
import org.forgerock.opendj.rest2ldap.Rest2LDAP;
import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
@@ -70,7 +73,6 @@
import org.glassfish.grizzly.http.server.NetworkListener;
import org.glassfish.grizzly.http.server.ServerConfiguration;
import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
import org.glassfish.grizzly.servlet.FilterRegistration;
import org.glassfish.grizzly.servlet.ServletRegistration;
import org.glassfish.grizzly.servlet.WebappContext;
import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
@@ -714,18 +716,24 @@
      final String urlPattern = "/*";
      final WebappContext ctx = new WebappContext(servletName);
      Filter filter = new CollectClientConnectionsFilter(this);
      final JsonValue configuration =
          parseJsonConfiguration(getFileForPath(this.currentConfig
              .getConfigFile()));
      final HTTPAuthenticationConfig authenticationConfig =
          getAuthenticationConfig(configuration);
      javax.servlet.Filter filter =
          new CollectClientConnectionsFilter(this, authenticationConfig);
      FilterRegistration filterReg =
          ctx.addFilter("collectClientConnections", filter);
      // TODO JNR this is not working
      // filterReg.addMappingForServletNames(EnumSet.allOf(
      // DispatcherType.class), servletName);
      filterReg.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),
          urlPattern);
          true, urlPattern);
      ConnectionFactory connFactory =
          getConnectionFactory(getFileForPath(this.currentConfig
              .getConfigFile()));
      ConnectionFactory connFactory = getConnectionFactory(configuration);
      final ServletRegistration reg =
          ctx.addServlet(servletName, new HttpServlet(connFactory,
@@ -757,19 +765,38 @@
    }
  }
  private ConnectionFactory getConnectionFactory(File configFile)
      throws Exception
  private HTTPAuthenticationConfig getAuthenticationConfig(
      final JsonValue configuration)
  {
    // Parse the config file.
    final Object content = JSON_MAPPER.readValue(configFile, Object.class);
    if (!(content instanceof Map))
    {
      throw new ServletException("Servlet configuration file '" + configFile
          + "' does not contain a valid JSON configuration");
    }
    final JsonValue configuration = new JsonValue(content);
    final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig();
    final JsonValue val = configuration.get("authenticationFilter");
    result.setBasicAuthenticationSupported(asBool(val,
        "supportHTTPBasicAuthentication"));
    result.setCustomHeadersAuthenticationSupported(asBool(val,
        "supportAltAuthentication"));
    result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader")
        .asString());
    result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader")
        .asString());
    final String searchBaseDN = asString(val, "searchBaseDN");
    result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN));
    result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope")));
    result.setSearchFilterTemplate(asString(val, "searchFilterTemplate"));
    return result;
  }
    // Create the router.
  private String asString(JsonValue value, String key)
  {
    return value.get(key).required().asString();
  }
  private boolean asBool(JsonValue value, String key)
  {
    return value.get(key).defaultTo(false).asBoolean();
  }
  private ConnectionFactory getConnectionFactory(final JsonValue configuration)
  {
    final Router router = new Router();
    final JsonValue mappings =
        configuration.get("servlet").get("mappings").required();
@@ -784,6 +811,23 @@
    return Resources.newInternalConnectionFactory(router);
  }
  private JsonValue parseJsonConfiguration(File configFile) throws IOException,
      JsonParseException, JsonMappingException, ServletException
  {
    // Parse the config file.
    final Object content = JSON_MAPPER.readValue(configFile, Object.class);
    if (!(content instanceof Map))
    {
      throw new ServletException("Servlet configuration file '" + configFile
          + "' does not contain a valid JSON configuration");
    }
    // TODO JNR should we restrict the possible configurations in this file?
    // Should we remove any config that does not make any sense to the
    // HTTP Connection Handler?
    return new JsonValue(content);
  }
  private void stopHttpServer()
  {
    TRACER.debugInfo("Stopping HTTP server...");
opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java
@@ -31,6 +31,7 @@
import static org.opends.server.loggers.debug.DebugLogger.*;
import java.util.LinkedHashSet;
import java.util.concurrent.atomic.AtomicInteger;
import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
import org.forgerock.opendj.ldap.ConnectionEventListener;
@@ -49,16 +50,20 @@
import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
import org.forgerock.opendj.ldap.requests.UnbindRequest;
import org.forgerock.opendj.ldap.responses.BindResult;
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.opends.server.core.BindOperationBasis;
import org.opends.server.core.QueueingStrategy;
import org.opends.server.core.SearchOperationBasis;
import org.opends.server.core.WorkQueueStrategy;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.ByteString;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.Operation;
import com.forgerock.opendj.util.AsynchronousFutureResult;
@@ -76,11 +81,11 @@
  /** The HTTP client connection being "adapted". */
  private final HTTPClientConnection clientConnection;
  /** FIXME: do not use constants. */
  private int messageID;
  /** FIXME: do not use constants. */
  private long operationID;
  /**
   * The next message ID (and operation ID) that should be used for this
   * connection.
   */
  private AtomicInteger nextMessageID = new AtomicInteger(0);
  /** The queueing strategy used for this connection. */
  private QueueingStrategy queueingStrategy = new WorkQueueStrategy();
@@ -102,6 +107,34 @@
    this.clientConnection = clientConnection;
  }
  private <R extends Result> FutureResult<R> enqueueOperation(
      Operation operation, ResultHandler<? super R> resultHandler)
  {
    // TODO JNR set requestID, but where to get it?
    final AsynchronousFutureResult<R, ResultHandler<? super R>> futureResult =
       new AsynchronousFutureResult<R, ResultHandler<? super R>>(resultHandler);
    try
    {
      clientConnection.addOperationInProgress(operation,
          (AsynchronousFutureResult) futureResult);
      queueingStrategy.enqueueRequest(operation);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      clientConnection.removeOperationInProgress(operation.getMessageID());
      // TODO JNR add error message??
      futureResult.handleErrorResult(ErrorResultException.newErrorResult(
          ResultCode.OPERATIONS_ERROR, e));
    }
    return futureResult;
  }
  /** {@inheritDoc} */
  @Override
  public FutureResult<Void> abandonAsync(AbandonRequest request)
@@ -146,12 +179,15 @@
      IntermediateResponseHandler intermediateResponseHandler,
      ResultHandler<? super BindResult> resultHandler)
  {
    // BindOperationBasis operation =
    // new BindOperationBasis(clientConnection, operationID, messageID,
    // to(request.getControls()), "3", to(request.getName()), "",
    // getCredentials(new byte[] {}));
    // TODO Auto-generated method stub
    throw new RuntimeException("Not implemented");
    int messageID = nextMessageID.get();
    String userName = request.getName();
    byte[] password = ((SimpleBindRequest) request).getPassword();
    BindOperationBasis operation =
        new BindOperationBasis(clientConnection, messageID, messageID,
            to(request.getControls()), "3", to(userName), ByteString
                .wrap(password));
    return enqueueOperation(operation, resultHandler);
  }
  /** {@inheritDoc} */
@@ -243,35 +279,15 @@
  {
    // TODO JNR attributes
    LinkedHashSet<String> attributes = null;
    SearchOperationBasis op2 =
        new SearchOperationBasis(clientConnection, operationID, messageID,
    final int messageID = nextMessageID.getAndIncrement();
    SearchOperationBasis operation =
        new SearchOperationBasis(clientConnection, messageID, messageID,
            to(request.getControls()), to(valueOf(request.getName())),
            to(request.getScope()), to(request.getDereferenceAliasesPolicy()),
            request.getSizeLimit(), request.getTimeLimit(), request
                .isTypesOnly(), to(request.getFilter()), attributes);
    // TODO JNR set requestID
    final AsynchronousFutureResult<Result, SearchResultHandler> futureResult =
       new AsynchronousFutureResult<Result, SearchResultHandler>(resultHandler);
    try
    {
      clientConnection.addOperationInProgress(op2, futureResult);
      queueingStrategy.enqueueRequest(op2);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      clientConnection.removeOperationInProgress(messageID);
      // TODO JNR add error message??
      futureResult.handleErrorResult(ErrorResultException.newErrorResult(
          ResultCode.OPERATIONS_ERROR, e));
    }
    return futureResult;
    return enqueueOperation(operation, resultHandler);
  }
  /** {@inheritDoc} */
opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
New file
@@ -0,0 +1,137 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2013 ForgeRock AS
 */
package org.opends.server.protocols.http;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.util.Base64;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class CollectClientConnectionsFilterTest extends DirectoryServerTestCase
{
  private static final String AUTHORIZATION =
      CollectClientConnectionsFilter.HTTP_BASIC_AUTH_HEADER;
  private static final String USERNAME = "Aladdin";
  private static final String PASSWORD = "open sesame";
  private static final String BASE64_USERPASS = Base64
      .encode((USERNAME + ":" + PASSWORD).getBytes());
  private HTTPAuthenticationConfig authConfig = new HTTPAuthenticationConfig();
  private CollectClientConnectionsFilter filter =
      new CollectClientConnectionsFilter(null, authConfig);
  @DataProvider(name = "Invalid HTTP basic auth strings")
  public Object[][] getInvalidHttpBasicAuthStrings()
  {
    return new Object[][] { { null }, { "bla" },
      { "basic " + Base64.encode("la:bli:blu".getBytes()) } };
  }
  @Test(dataProvider = "Invalid HTTP basic auth strings")
  public void parseUsernamePasswordFromInvalidAuthZHeader(String authZHeader)
  {
    assertThat(filter.parseUsernamePassword(authZHeader)).isNull();
  }
  @DataProvider(name = "Valid HTTP basic auth strings")
  public Object[][] getValidHttpBasicAuthStrings()
  {
    return new Object[][] { { "basic " + BASE64_USERPASS },
      { "Basic " + BASE64_USERPASS } };
  }
  @Test(dataProvider = "Valid HTTP basic auth strings")
  public void parseUsernamePasswordFromValidAuthZHeader(String authZHeader)
  {
    assertThat(filter.parseUsernamePassword(authZHeader)).containsExactly(
        USERNAME, PASSWORD);
  }
  @Test
  public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent()
  {
    authConfig.setBasicAuthenticationSupported(true);
    HttpServletResponse response = mock(HttpServletResponse.class);
    filter.sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    verify(response).setHeader("WWW-Authenticate",
        "Basic realm=\"org.forgerock.opendj\"");
  }
  @Test
  public void sendUnauthorizedResponseWithoutHttpBasicAuthWillNotChallengeUserAgent()
  {
    authConfig.setBasicAuthenticationSupported(true);
    HttpServletResponse response = mock(HttpServletResponse.class);
    filter.sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  }
  @Test
  public void extractUsernamePasswordHttpBasicAuthWillAcceptUserAgent()
  {
    authConfig.setBasicAuthenticationSupported(true);
    HttpServletRequest request = mock(HttpServletRequest.class);
    when(request.getHeader(AUTHORIZATION)).thenReturn(
        "Basic " + BASE64_USERPASS);
    assertThat(filter.extractUsernamePassword(request)).containsExactly(
        USERNAME, PASSWORD);
  }
  @Test
  public void extractUsernamePasswordCustomHeaders()
  {
    final String customHeaderUsername = "X-OpenIDM-Username";
    final String customHeaderPassword = "X-OpenIDM-Password";
    authConfig.setCustomHeadersAuthenticationSupported(true);
    authConfig.setCustomHeaderUsername(customHeaderUsername);
    authConfig.setCustomHeaderPassword(customHeaderPassword);
    HttpServletRequest request = mock(HttpServletRequest.class);
    when(request.getHeader(customHeaderUsername)).thenReturn(USERNAME);
    when(request.getHeader(customHeaderPassword)).thenReturn(PASSWORD);
    assertThat(filter.extractUsernamePassword(request)).containsExactly(
        USERNAME, PASSWORD);
  }
}