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