From feb6f6f0af2cbb3ebfb91f1bf546c23669b75e4a Mon Sep 17 00:00:00 2001
From: Jean-Noel Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Tue, 16 Apr 2013 13:57:43 +0000
Subject: [PATCH] OPENDJ-830 (CR-1554) Implement authentication and authorization for HTTP connection handler

---
 opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java                                       |    2 
 opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java                             |  110 +++++++++++++++++++++---------------
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java |   26 ++++++--
 3 files changed, 84 insertions(+), 54 deletions(-)

diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
index 1584322..7d99dab 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -48,6 +48,7 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.forgerock.json.resource.ResourceException;
 import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.ErrorResultException;
@@ -58,6 +59,7 @@
 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.Rest2LDAP;
 import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
 import org.opends.messages.Message;
 import org.opends.server.admin.std.server.ConnectionHandlerCfg;
@@ -119,6 +121,9 @@
   public void doFilter(ServletRequest request, ServletResponse response,
       FilterChain chain)
   {
+    final boolean prettyPrint =
+        Boolean.parseBoolean(request.getParameter("_prettyPrint"));
+
     final HTTPClientConnection clientConnection =
         new HTTPClientConnection(this.connectionHandler, request);
     this.connectionHandler.addClientConnection(clientConnection);
@@ -156,7 +161,9 @@
 
       // The user could not be authenticated. Send an HTTP Basic authentication
       // challenge if HTTP Basic authentication is enabled.
-      sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
+      sendErrorReponse(response, prettyPrint, ResourceException.getException(
+          HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials"));
+
       clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS, false,
           null);
     }
@@ -167,6 +174,8 @@
         TRACER.debugCaught(DebugLogLevel.ERROR, e);
       }
 
+      sendErrorReponse(response, prettyPrint, Rest2LDAP.asResourceException(e));
+
       Message message =
           INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(clientConnection
               .getClientHostPort(), clientConnection.getServerHostPort(),
@@ -262,8 +271,11 @@
    *          the request where to extract the username and password from
    * @return the array containing the username/password couple if both exist,
    *         null otherwise
+   * @throws ResourceException
+   *           if any error occur
    */
   String[] extractUsernamePassword(ServletRequest request)
+      throws ResourceException
   {
     HttpServletRequest req = (HttpServletRequest) request;
 
@@ -299,19 +311,25 @@
   }
 
   /**
-   * Sends an Unauthorized status code and a challenge for HTTP Basic
-   * authentication if HTTP basic authentication is enabled.
+   * Sends an error response back to the client. If the error response is
+   * "Unauthorized", then it will send a challenge for HTTP Basic authentication
+   * if HTTP Basic authentication is enabled.
    *
    * @param response
    *          where to send the Unauthorized status code.
+   * @param prettyPrint
+   *          whether to format the JSON document output
+   * @param re
+   *          the resource exception with the error response content
    */
-  void sendUnauthorizedResponseWithHTTPBasicAuthChallenge(
-      ServletResponse response)
+  void sendErrorReponse(ServletResponse response, boolean prettyPrint,
+      ResourceException re)
   {
     HttpServletResponse resp = (HttpServletResponse) response;
-    resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+    resp.setStatus(re.getCode());
 
-    if (authConfig.isBasicAuthenticationSupported())
+    if (re.getCode() == HttpServletResponse.SC_UNAUTHORIZED
+        && authConfig.isBasicAuthenticationSupported())
     {
       resp.setHeader("WWW-Authenticate",
           "Basic realm=\"org.forgerock.opendj\"");
@@ -323,11 +341,7 @@
       resp.setHeader("Content-Type", "application/json");
 
       ServletOutputStream out = resp.getOutputStream();
-      out.println("{");
-      out.println("    \"code\": 401,");
-      out.println("    \"message\": \"Invalid Credentials\",");
-      out.println("    \"reason\": \"Unauthorized\"");
-      out.println("}");
+      out.println(toJSON(prettyPrint, re));
     }
     catch (IOException ignore)
     {
@@ -340,6 +354,32 @@
   }
 
   /**
+   * Returns a JSON representation of the {@link ResourceException}.
+   *
+   * @param prettyPrint
+   *          whether to format the resulting JSON document
+   * @param re
+   *          the resource exception to convert to a JSON document
+   * @return a String containing the JSON representation of the
+   *         {@link ResourceException}.
+   */
+  private String toJSON(boolean prettyPrint, ResourceException re)
+  {
+    final String indent = "\n    ";
+    final StringBuilder sb = new StringBuilder();
+    sb.append("{");
+    if (prettyPrint) sb.append(indent);
+    sb.append("\"code\": ").append(re.getCode()).append(",");
+    if (prettyPrint) sb.append(indent);
+    sb.append("\"message\": \"").append(re.getMessage()).append("\",");
+    if (prettyPrint) sb.append(indent);
+    sb.append("\"reason\": \"").append(re.getReason()).append("\"");
+    if (prettyPrint) sb.append("\n");
+    sb.append("}");
+    return sb.toString();
+  }
+
+  /**
    * Parses username and password from the authentication header used in HTTP
    * basic authentication.
    *
@@ -347,8 +387,10 @@
    *          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
+   * @throws ResourceException
+   *           if the base64 password cannot be decoded
    */
-  String[] parseUsernamePassword(String authHeader)
+  String[] parseUsernamePassword(String authHeader) throws ResourceException
   {
     if (authHeader != null
         && (authHeader.startsWith("Basic") || authHeader.startsWith("basic")))
@@ -370,10 +412,7 @@
       }
       catch (ParseException e)
       {
-        if (debugEnabled())
-        {
-          TRACER.debugCaught(DebugLogLevel.ERROR, e);
-        }
+        throw Rest2LDAP.asResourceException(e);
       }
     }
     return null;
@@ -392,9 +431,11 @@
    *          the connection to use for search and bind
    * @return the {@link AuthenticationInfo} for the supplied credentials, null
    *         if authentication was unsuccessful.
+   * @throws ErrorResultException
+   *           if a Rest2LDAP result comes back with an error or cancel state
    */
   private AuthenticationInfo authenticate(String userName, String password,
-      Connection connection)
+      Connection connection) throws ErrorResultException
   {
     // TODO JNR do the next steps in an async way
     SearchResultEntry resultEntry = searchUniqueEntryDN(userName, connection);
@@ -411,7 +452,7 @@
   }
 
   private SearchResultEntry searchUniqueEntryDN(String userName,
-      Connection connection)
+      Connection connection) throws ErrorResultException
   {
     // use configured rights to find the user DN
     final Filter filter =
@@ -419,37 +460,16 @@
     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;
+    return connection.searchSingleEntry(searchRequest);
   }
 
   private boolean bind(String bindDN, String password, Connection connection)
+      throws ErrorResultException
   {
-    BindRequest bindRequest =
+    final 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;
+    final BindResult bindResult = connection.bind(bindRequest);
+    return ResultCode.SUCCESS.equals(bindResult.getResultCode());
   }
 
   /** {@inheritDoc} */
diff --git a/opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java b/opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
index 361d045..8d82c76 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/protocols/http/HTTPClientConnection.java
@@ -119,7 +119,7 @@
    * Indicates whether the Directory Server believes this connection to be valid
    * and available for communication.
    */
-  private volatile boolean connectionValid;
+  private volatile boolean connectionValid = true;
 
   /**
    * Indicates whether this connection is about to be closed. This will be used
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
index dfd09d6..5e18241 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
@@ -36,6 +36,7 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.forgerock.json.resource.ResourceException;
 import org.opends.server.DirectoryServerTestCase;
 import org.opends.server.types.AuthenticationInfo;
 import org.opends.server.util.Base64;
@@ -65,6 +66,7 @@
 
   @Test(dataProvider = "Invalid HTTP basic auth strings")
   public void parseUsernamePasswordFromInvalidAuthZHeader(String authZHeader)
+      throws Exception
   {
     assertThat(filter.parseUsernamePassword(authZHeader)).isNull();
   }
@@ -78,6 +80,7 @@
 
   @Test(dataProvider = "Valid HTTP basic auth strings")
   public void parseUsernamePasswordFromValidAuthZHeader(String authZHeader)
+      throws Exception
   {
     assertThat(filter.parseUsernamePassword(authZHeader)).containsExactly(
         USERNAME, PASSWORD);
@@ -92,7 +95,7 @@
     ServletOutputStream oStream = mock(ServletOutputStream.class);
     HttpServletResponse response = mock(HttpServletResponse.class);
     when(response.getOutputStream()).thenReturn(oStream);
-    filter.sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
+    sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
 
     verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     verify(response).setHeader("WWW-Authenticate",
@@ -109,25 +112,32 @@
     HttpServletResponse response = mock(HttpServletResponse.class);
     ServletOutputStream oStream = mock(ServletOutputStream.class);
     when(response.getOutputStream()).thenReturn(oStream);
-    filter.sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
+    sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
 
     verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     verifyUnauthorizedOutputMessage(response, oStream);
   }
 
+  private void sendUnauthorizedResponseWithHTTPBasicAuthChallenge(
+      HttpServletResponse response)
+  {
+    filter.sendErrorReponse(response, true, ResourceException.getException(
+        HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials"));
+  }
+
   private void verifyUnauthorizedOutputMessage(HttpServletResponse response,
       ServletOutputStream oStream) throws IOException
   {
     verify(response).getOutputStream();
-    verify(oStream).println("{");
-    verify(oStream).println("    \"code\": 401,");
-    verify(oStream).println("    \"message\": \"Invalid Credentials\",");
-    verify(oStream).println("    \"reason\": \"Unauthorized\"");
-    verify(oStream).println("}");
+    verify(oStream).println(
+        "{\n" + "    \"code\": 401,\n"
+            + "    \"message\": \"Invalid Credentials\",\n"
+            + "    \"reason\": \"Unauthorized\"\n" + "}");
   }
 
   @Test
   public void extractUsernamePasswordHttpBasicAuthWillAcceptUserAgent()
+      throws Exception
   {
     authConfig.setBasicAuthenticationSupported(true);
 
@@ -140,7 +150,7 @@
   }
 
   @Test
-  public void extractUsernamePasswordCustomHeaders()
+  public void extractUsernamePasswordCustomHeaders() throws Exception
   {
     final String customHeaderUsername = "X-OpenIDM-Username";
     final String customHeaderPassword = "X-OpenIDM-Password";

--
Gitblit v1.10.0