From 202b9cdeebbd40337f0de6ae106d6b5293d1ea46 Mon Sep 17 00:00:00 2001
From: Jean-Noel Rouvignac <jean-noel.rouvignac@forgerock.com>
Date: Tue, 23 Apr 2013 13:13:13 +0000
Subject: [PATCH] OPENDJ-830 (CR-1591) Implement authentication and authorization for HTTP connection handler
---
opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java | 4
opends/tests/unit-tests-testng/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java | 20 ---
opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java | 294 +++++++++++++++++++++++++++++++-----------------
3 files changed, 190 insertions(+), 128 deletions(-)
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 7d99dab..9ee9af3 100644
--- a/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
+++ b/opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -36,9 +36,11 @@
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.nio.charset.Charset;
import java.text.ParseException;
import java.util.Collection;
+import javax.servlet.AsyncContext;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
@@ -53,7 +55,7 @@
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.ResultHandler;
import org.forgerock.opendj.ldap.requests.BindRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
@@ -78,6 +80,105 @@
final class CollectClientConnectionsFilter implements javax.servlet.Filter
{
+ /** This class holds all the necessary data to complete an HTTP request. */
+ private final class HTTPRequestContext
+ {
+ private AsyncContext asyncContext;
+ private ServletRequest request;
+ private ServletResponse response;
+ private FilterChain chain;
+
+ private HTTPClientConnection clientConnection;
+ private Connection connection;
+
+ /** Whether to pretty print the resulting JSON. */
+ private boolean prettyPrint;
+ /** Can be used for the bind request. */
+ private String password;
+ }
+
+ /**
+ * This result handler calls {@link javax.servlet.Filter#doFilter()} after a
+ * successful bind.
+ */
+ private final class CallDoFilterResultHandler implements
+ ResultHandler<BindResult>
+ {
+
+ private final HTTPRequestContext ctx;
+ private final SearchResultEntry resultEntry;
+
+ private CallDoFilterResultHandler(HTTPRequestContext ctx,
+ SearchResultEntry resultEntry)
+ {
+ this.ctx = ctx;
+ this.resultEntry = resultEntry;
+ }
+
+ @Override
+ public void handleErrorResult(ErrorResultException error)
+ {
+ onFailure(error, ctx);
+ }
+
+ @Override
+ public void handleResult(BindResult result)
+ {
+ final AuthenticationInfo authInfo =
+ new AuthenticationInfo(to(resultEntry), to(resultEntry.getName()),
+ ByteString.valueOf(ctx.password), false);
+ try
+ {
+ doFilter(ctx, authInfo);
+ }
+ catch (Exception e)
+ {
+ onFailure(e, ctx);
+ }
+ }
+
+ }
+
+ /**
+ * This result handler invokes a bind after a successful search on the user
+ * name used for authentication.
+ */
+ private final class DoBindResultHandler implements
+ ResultHandler<SearchResultEntry>
+ {
+ private HTTPRequestContext ctx;
+
+ private DoBindResultHandler(HTTPRequestContext ctx)
+ {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public void handleErrorResult(ErrorResultException error)
+ {
+ onFailure(error, ctx);
+ }
+
+ @Override
+ public void handleResult(SearchResultEntry resultEntry)
+ {
+ final DN bindDN = resultEntry.getName();
+ if (bindDN == null)
+ {
+ sendAuthenticationFailure(ctx);
+ }
+ else
+ {
+ final BindRequest bindRequest =
+ Requests.newSimpleBindRequest(bindDN.toString(), ctx.password
+ .getBytes(Charset.forName("UTF-8")));
+ ctx.connection.bindAsync(bindRequest, null,
+ new CallDoFilterResultHandler(ctx, resultEntry));
+ }
+ }
+
+ }
+
/** HTTP Header sent by the client with HTTP basic authentication. */
static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
@@ -127,6 +228,15 @@
final HTTPClientConnection clientConnection =
new HTTPClientConnection(this.connectionHandler, request);
this.connectionHandler.addClientConnection(clientConnection);
+
+ final HTTPRequestContext ctx = new HTTPRequestContext();
+ ctx.request = request;
+ ctx.response = response;
+ ctx.chain = chain;
+
+ ctx.clientConnection = clientConnection;
+ ctx.prettyPrint = prettyPrint;
+
try
{
if (!canProcessRequest(request, clientConnection))
@@ -137,53 +247,95 @@
// checked.
logConnect(clientConnection);
- Connection connection = new SdkConnectionAdapter(clientConnection);
+ ctx.connection = new SdkConnectionAdapter(clientConnection);
- AuthenticationInfo authInfo = getAuthenticationInfo(request, connection);
- if (authInfo != null)
+ final String[] userPassword = extractUsernamePassword(request);
+ if (userPassword != null && userPassword.length == 2)
{
- clientConnection.setAuthenticationInfo(authInfo);
+ final String userName = userPassword[0];
+ ctx.password = userPassword[1];
- /*
- * 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);
+ final AsyncContext asyncContext = getAsyncContext(request);
+ asyncContext.setTimeout(60 * 1000);
+ ctx.asyncContext = asyncContext;
- // send the request further down the filter chain or pass to servlet
- chain.doFilter(request, response);
- return;
+ ctx.connection.searchSingleEntryAsync(buildSearchRequest(userName),
+ new DoBindResultHandler(ctx));
}
-
- // The user could not be authenticated. Send an HTTP Basic authentication
- // challenge if HTTP Basic authentication is enabled.
- sendErrorReponse(response, prettyPrint, ResourceException.getException(
- HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials"));
-
- clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS, false,
- null);
+ else if (this.connectionHandler.acceptUnauthenticatedRequests())
+ {
+ // use unauthenticated user
+ doFilter(ctx, new AuthenticationInfo());
+ }
+ else
+ {
+ sendAuthenticationFailure(ctx);
+ }
}
catch (Exception e)
{
+ onFailure(e, ctx);
+ }
+ }
+
+ private void doFilter(HTTPRequestContext ctx, AuthenticationInfo authInfo)
+ throws Exception
+ {
+ ctx.clientConnection.setAuthenticationInfo(authInfo);
+
+ /*
+ * 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.
+ */
+ ctx.request.setAttribute(
+ Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, ctx.connection);
+
+ // send the request further down the filter chain or pass to servlet
+ ctx.chain.doFilter(ctx.request, ctx.response);
+ }
+
+ private void sendAuthenticationFailure(HTTPRequestContext ctx)
+ {
+ // The user could not be authenticated. Send an HTTP Basic authentication
+ // challenge if HTTP Basic authentication is enabled.
+ ResourceException unauthorizedException =
+ ResourceException.getException(HttpServletResponse.SC_UNAUTHORIZED,
+ "Invalid Credentials");
+ sendErrorReponse(ctx.response, ctx.prettyPrint, unauthorizedException);
+
+ ctx.clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS,
+ false, null);
+ }
+
+ private void onFailure(Exception e, HTTPRequestContext ctx)
+ {
+ try
+ {
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
- sendErrorReponse(response, prettyPrint, Rest2LDAP.asResourceException(e));
+ sendErrorReponse(ctx.response, ctx.prettyPrint, Rest2LDAP
+ .asResourceException(e));
Message message =
- INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(clientConnection
- .getClientHostPort(), clientConnection.getServerHostPort(),
+ INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(ctx.clientConnection
+ .getClientHostPort(), ctx.clientConnection.getServerHostPort(),
getExceptionMessage(e));
logError(message);
- clientConnection
- .disconnect(DisconnectReason.SERVER_ERROR, false, message);
+ ctx.clientConnection.disconnect(DisconnectReason.SERVER_ERROR, false,
+ message);
+ }
+ finally
+ {
+ if (ctx.asyncContext != null)
+ {
+ ctx.asyncContext.complete();
+ }
}
}
@@ -230,37 +382,6 @@
}
/**
- * Returns an {@link AuthenticationInfo} object if the request is accepted. An
- * {@link AuthenticationInfo} object will be returned if authentication
- * credentials were valid or if unauthenticated requests are allowed on this
- * server.
- *
- * @param request
- * the request used to extract the {@link AuthenticationInfo}
- * @param connection
- * the connection used to retrieve the {@link AuthenticationInfo}
- * @return an {@link AuthenticationInfo} if the request is accepted, null if
- * the request was rejected
- * @throws Exception
- * if any problem occur
- */
- AuthenticationInfo getAuthenticationInfo(ServletRequest request,
- Connection connection) throws Exception
- {
- String[] userPassword = extractUsernamePassword(request);
- if (userPassword != null && userPassword.length == 2)
- {
- return authenticate(userPassword[0], userPassword[1], connection);
- }
- else if (this.connectionHandler.acceptUnauthenticatedRequests())
- {
- // return unauthenticated
- return new AuthenticationInfo();
- }
- return null;
- }
-
- /**
* 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
@@ -418,58 +539,19 @@
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.
- * @throws ErrorResultException
- * if a Rest2LDAP result comes back with an error or cancel state
- */
- private AuthenticationInfo authenticate(String userName, String password,
- Connection connection) throws ErrorResultException
+ private AsyncContext getAsyncContext(ServletRequest request)
{
- // 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;
+ return request.isAsyncStarted() ? request.getAsyncContext() : request
+ .startAsync();
}
- private SearchResultEntry searchUniqueEntryDN(String userName,
- Connection connection) throws ErrorResultException
+ private SearchRequest buildSearchRequest(String userName)
{
// 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);
- return connection.searchSingleEntry(searchRequest);
- }
-
- private boolean bind(String bindDN, String password, Connection connection)
- throws ErrorResultException
- {
- final BindRequest bindRequest =
- Requests.newSimpleBindRequest(bindDN, password.getBytes());
- final BindResult bindResult = connection.bind(bindRequest);
- return ResultCode.SUCCESS.equals(bindResult.getResultCode());
+ return Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig
+ .getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
}
/** {@inheritDoc} */
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 02f16b5..04188dc 100644
--- a/opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java
+++ b/opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java
@@ -193,7 +193,7 @@
IntermediateResponseHandler intermediateResponseHandler,
ResultHandler<? super BindResult> resultHandler)
{
- final int messageID = nextMessageID.get();
+ final int messageID = nextMessageID.getAndIncrement();
String userName = request.getName();
byte[] password = ((SimpleBindRequest) request).getPassword();
BindOperationBasis operation =
@@ -211,7 +211,7 @@
AuthenticationInfo authInfo = this.clientConnection.getAuthenticationInfo();
if (authInfo != null && authInfo.isAuthenticated())
{
- final int messageID = nextMessageID.get();
+ final int messageID = nextMessageID.getAndIncrement();
UnbindOperationBasis operation =
new UnbindOperationBasis(clientConnection, messageID, messageID,
to(request.getControls()));
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
index 5e18241..4a40807 100644
--- 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
@@ -38,7 +38,6 @@
import org.forgerock.json.resource.ResourceException;
import org.opends.server.DirectoryServerTestCase;
-import org.opends.server.types.AuthenticationInfo;
import org.opends.server.util.Base64;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -167,23 +166,4 @@
USERNAME, PASSWORD);
}
- /**
- * Tests that getAuthenticationInfo() without basic auth header or custom
- * headers returns an unauthenticated info when the server accepts
- * unauthenticated requests.
- */
- @Test
- public void getAuthenticationInfoReturnsUnauthenticatedInfo()
- throws Exception
- {
- HttpServletRequest request = mock(HttpServletRequest.class);
- HTTPConnectionHandler cfg = mock(HTTPConnectionHandler.class);
- when(cfg.acceptUnauthenticatedRequests()).thenReturn(true);
-
- filter = new CollectClientConnectionsFilter(cfg, authConfig);
- AuthenticationInfo authInfo = filter.getAuthenticationInfo(request, null);
- assertThat(authInfo).isNotNull();
- assertThat(authInfo.isAuthenticated()).isFalse();
- }
-
}
--
Gitblit v1.10.0