From 503283bea9d628b1d118107d62a7c0fd6df61fce Mon Sep 17 00:00:00 2001
From: Jean-Noel Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Fri, 05 Apr 2013 09:21:58 +0000
Subject: [PATCH] OPENDJ-830 (CR-1505) Implement authentication and authorization for HTTP connection handler

---
 opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java                                       |   22 +
 opends/src/server/org/opends/server/protocols/http/HTTPAuthenticationConfig.java                                   |  233 ++++++++++++++++
 opends/build.xml                                                                                                   |   15 +
 opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java                                       |   86 +++--
 opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java |  137 +++++++++
 opends/ivy.xml                                                                                                     |    9 
 opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java                             |  254 +++++++++++++++++-
 opends/src/server/org/opends/server/protocols/http/HTTPConnectionHandler.java                                      |   80 ++++-
 8 files changed, 762 insertions(+), 74 deletions(-)

diff --git a/opends/build.xml b/opends/build.xml
index f380df3..ab5178e 100644
--- a/opends/build.xml
+++ b/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" />
diff --git a/opends/ivy.xml b/opends/ivy.xml
index 5eaecf2..b28b8d3 100644
--- a/opends/ivy.xml
+++ b/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"/>
diff --git a/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java b/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
index b7b06a5..018c06c 100644
--- a/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
+++ b/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()
diff --git a/opends/src/server/org/opends/server/protocols/http/HTTPAuthenticationConfig.java b/opends/src/server/org/opends/server/protocols/http/HTTPAuthenticationConfig.java
new file mode 100644
index 0000000..00839c3
--- /dev/null
+++ b/opends/src/server/org/opends/server/protocols/http/HTTPAuthenticationConfig.java
@@ -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();
+  }
+}
diff --git a/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java b/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
index 0306288..3cb3d59 100644
--- a/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
+++ b/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)
     {
diff --git a/opends/src/server/org/opends/server/protocols/http/HTTPConnectionHandler.java b/opends/src/server/org/opends/server/protocols/http/HTTPConnectionHandler.java
index 4dfaff1..2ac6020 100644
--- a/opends/src/server/org/opends/server/protocols/http/HTTPConnectionHandler.java
+++ b/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...");
diff --git a/opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java b/opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java
index d5d2c22..ed97979 100644
--- a/opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java
+++ b/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} */
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
new file mode 100644
index 0000000..010b0aa
--- /dev/null
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
@@ -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);
+  }
+
+}

--
Gitblit v1.10.0