| | |
| | | import jakarta.servlet.WriteListener; |
| | | import jakarta.servlet.http.HttpServletRequest; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | |
| | | import org.opends.server.tools.LDAPReader; |
| | | import org.opends.server.tools.LDAPWriter; |
| | | import org.testng.annotations.Test; |
| | | import org.w3c.dom.Document; |
| | | import org.w3c.dom.Element; |
| | | import org.w3c.dom.NodeList; |
| | | |
| | | /** |
| | | * Tests the error handling of {@link DSMLServlet#doPost}: an abandon request |
| | | * used to trigger a {@code NullPointerException} which leaked the LDAP |
| | | * connection, a request without a usable Content-Type header used to trigger a |
| | | * {@code NullPointerException} as well, and the second batch request of a SOAP |
| | | * body used to be silently skipped. Also covers the caps on the number of |
| | | * batchRequest elements per SOAP body (each element costs a bind) and on the |
| | | * size of the request body. |
| | | * {@code NullPointerException} as well, the second batch request of a SOAP |
| | | * body used to be silently skipped, and every batch request of a SOAP body |
| | | * used to be answered inside a single shared batchResponse. Also covers the |
| | | * caps on the number of batchRequest elements per SOAP body (each element |
| | | * costs a bind) and on the size of the request body. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | @Test(groups = { "precommit", "dsml" }) |
| | |
| | | private static final String SOAP_1_2_CONTENT_TYPE = "application/soap+xml"; |
| | | /** SOAP 1.2 envelope namespace, as it appears in the reply. */ |
| | | private static final String SOAP_1_2_NAMESPACE = "http://www.w3.org/2003/05/soap-envelope"; |
| | | /** DSMLv2 core namespace. */ |
| | | private static final String DSML_NAMESPACE = "urn:oasis:names:tc:DSML:2:0:core"; |
| | | |
| | | private static final String ABANDON_BATCH = |
| | | soap11(abandonBatch("1", null)); |
| | |
| | | |
| | | private static String abandonBatch(String requestID, String authzPrincipal) |
| | | { |
| | | return "<batchRequest xmlns=\"urn:oasis:names:tc:DSML:2:0:core\" requestID=\"" + requestID + "\">" |
| | | return "<batchRequest xmlns=\"" + DSML_NAMESPACE + "\" requestID=\"" + requestID + "\">" |
| | | + (authzPrincipal != null ? "<authRequest principal=\"" + authzPrincipal + "\"/>" : "") |
| | | + "<abandonRequest abandonID=\"1\"/>" |
| | | + "</batchRequest>"; |
| | |
| | | * bind, so by default a single POST may only hold one: the excess must be |
| | | * rejected without being executed, not silently skipped, and the results of |
| | | * the elements under the cap must still reach the client next to the error. |
| | | * The error joins the answered batchResponse instead of forming a root of |
| | | * its own: DSMLv2 expects a single batchResponse per SOAP body, and the |
| | | * default configuration must not produce a two-root reply. |
| | | */ |
| | | @Test |
| | | public void testExcessBatchRequestsAreRejectedByDefault() throws Exception |
| | |
| | | |
| | | String response = doPost(server.getPort(), headers, SEARCH_AND_ABANDON_BATCHES); |
| | | |
| | | assertTrue(response.contains("searchResponse"), response); |
| | | assertTrue(response.contains("notAttempted"), response); |
| | | List<Element> replies = batchResponsesOf(response); |
| | | assertEquals(replies.size(), 1, |
| | | "the default configuration must answer with a single batchResponse root: " + response); |
| | | assertEquals(replies.get(0).getAttribute("requestID"), "1", response); |
| | | List<Element> elements = childElements(replies.get(0)); |
| | | assertEquals(elements.size(), 2, response); |
| | | assertEquals(elements.get(0).getLocalName(), "searchResponse", response); |
| | | assertEquals(elements.get(1).getLocalName(), "errorResponse", response); |
| | | assertEquals(elements.get(1).getAttribute("type"), "notAttempted", response); |
| | | |
| | | server.awaitDisconnect(); |
| | | assertEquals(server.getReceivedOpTypes(), |
| | |
| | | } |
| | | |
| | | /** |
| | | * Every batchRequest of a SOAP body is answered with a batchResponse of its |
| | | * own: a single shared one used to merge the elements of every batch |
| | | * request and to keep only the requestID of the last one. |
| | | */ |
| | | @Test |
| | | public void testEachBatchRequestGetsItsOwnBatchResponse() throws Exception |
| | | { |
| | | try (FakeLdapServer server = new FakeLdapServer()) |
| | | { |
| | | String response = doAuthzPost(server, TWO_AUTHZ_BATCHES); |
| | | |
| | | server.awaitDisconnect(2); |
| | | List<Element> replies = batchResponsesOf(response); |
| | | assertEquals(replies.size(), 2, response); |
| | | assertEquals(replies.get(0).getAttribute("requestID"), "1", response); |
| | | assertEquals(replies.get(1).getAttribute("requestID"), "2", response); |
| | | for (Element reply : replies) |
| | | { |
| | | List<Element> elements = childElements(reply); |
| | | assertEquals(elements.size(), 1, |
| | | "each batch request must keep its response elements to itself: " + response); |
| | | assertEquals(elements.get(0).getLocalName(), "authResponse", response); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A batchRequest which fails schema validation is answered inside its own |
| | | * batchResponse, under its own requestID: the SAX fallback used to recover |
| | | * the requestID of the first batchRequest of the body instead. The cap on |
| | | * batchRequest elements has to be raised to let two of them in. |
| | | */ |
| | | @Test |
| | | public void testMalformedBatchRequestIsAnsweredUnderItsOwnRequestID() throws Exception |
| | | { |
| | | try (FakeLdapServer server = new FakeLdapServer()) |
| | | { |
| | | Map<String, String> params = new LinkedHashMap<>(); |
| | | params.put("ldap.dsml.batchrequests.max", "2"); |
| | | |
| | | Map<String, String> headers = new LinkedHashMap<>(); |
| | | headers.put("Content-Type", SOAP_1_1_CONTENT_TYPE); |
| | | |
| | | String malformed = "<batchRequest xmlns=\"" + DSML_NAMESPACE + "\" requestID=\"2\">" |
| | | + "<bogusRequest/></batchRequest>"; |
| | | String response = |
| | | doPost(server.getPort(), params, headers, soap11(abandonBatch("1", null) + malformed)); |
| | | |
| | | server.awaitDisconnect(); |
| | | List<Element> replies = batchResponsesOf(response); |
| | | assertEquals(replies.size(), 2, response); |
| | | assertEquals(replies.get(0).getAttribute("requestID"), "1", response); |
| | | assertTrue(childElements(replies.get(0)).isEmpty(), |
| | | "an abandon request produces no response element: " + response); |
| | | assertEquals(replies.get(1).getAttribute("requestID"), "2", response); |
| | | List<Element> errors = childElements(replies.get(1)); |
| | | assertEquals(errors.size(), 1, response); |
| | | assertEquals(errors.get(0).getLocalName(), "errorResponse", response); |
| | | assertEquals(errors.get(0).getAttribute("type"), "malformedRequest", response); |
| | | assertEquals(server.getReceivedOpTypes(), |
| | | list(OP_TYPE_BIND_REQUEST, OP_TYPE_ABANDON_REQUEST, OP_TYPE_UNBIND_REQUEST), |
| | | "only the valid batch request should have reached the directory server"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A malformed batchRequest without a requestID is answered inside a |
| | | * batchResponse which carries no requestID at all: an empty attribute would |
| | | * read as a requestID of "". |
| | | */ |
| | | @Test |
| | | public void testMalformedBatchRequestWithoutRequestIDIsAnsweredWithoutOne() throws Exception |
| | | { |
| | | try (FakeLdapServer server = new FakeLdapServer()) |
| | | { |
| | | Map<String, String> headers = new LinkedHashMap<>(); |
| | | headers.put("Content-Type", SOAP_1_1_CONTENT_TYPE); |
| | | |
| | | String malformed = "<batchRequest xmlns=\"" + DSML_NAMESPACE + "\">" |
| | | + "<bogusRequest/></batchRequest>"; |
| | | String response = doPost(server.getPort(), headers, soap11(malformed)); |
| | | |
| | | List<Element> replies = batchResponsesOf(response); |
| | | assertEquals(replies.size(), 1, response); |
| | | assertFalse(replies.get(0).hasAttribute("requestID"), |
| | | "a batch request without a requestID must be answered without one: " + response); |
| | | List<Element> errors = childElements(replies.get(0)); |
| | | assertEquals(errors.size(), 1, response); |
| | | assertEquals(errors.get(0).getLocalName(), "errorResponse", response); |
| | | assertEquals(errors.get(0).getAttribute("type"), "malformedRequest", response); |
| | | assertTrue(server.getReceivedOpTypes().isEmpty(), |
| | | "no connection to the directory server should have been opened"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * A SOAP body element which is no batchRequest at all is answered in place, |
| | | * as a malformed request, under the requestID read from the element itself. |
| | | */ |
| | | @Test |
| | | public void testNonBatchRequestElementIsAnsweredAsMalformed() throws Exception |
| | | { |
| | | try (FakeLdapServer server = new FakeLdapServer()) |
| | | { |
| | | Map<String, String> params = new LinkedHashMap<>(); |
| | | params.put("ldap.dsml.batchrequests.max", "2"); |
| | | |
| | | Map<String, String> headers = new LinkedHashMap<>(); |
| | | headers.put("Content-Type", SOAP_1_1_CONTENT_TYPE); |
| | | |
| | | String alien = "<somethingElse xmlns=\"urn:example:not-dsml\" requestID=\"7\"/>"; |
| | | String response = |
| | | doPost(server.getPort(), params, headers, soap11(abandonBatch("1", null) + alien)); |
| | | |
| | | server.awaitDisconnect(); |
| | | List<Element> replies = batchResponsesOf(response); |
| | | assertEquals(replies.size(), 2, response); |
| | | assertEquals(replies.get(0).getAttribute("requestID"), "1", response); |
| | | assertEquals(replies.get(1).getAttribute("requestID"), "7", response); |
| | | List<Element> errors = childElements(replies.get(1)); |
| | | assertEquals(errors.size(), 1, response); |
| | | assertEquals(errors.get(0).getLocalName(), "errorResponse", response); |
| | | assertEquals(errors.get(0).getAttribute("type"), "malformedRequest", response); |
| | | assertEquals(server.getReceivedOpTypes(), |
| | | list(OP_TYPE_BIND_REQUEST, OP_TYPE_ABANDON_REQUEST, OP_TYPE_UNBIND_REQUEST), |
| | | "only the batchRequest should have reached the directory server"); |
| | | } |
| | | } |
| | | |
| | | /** A SOAP body without any batchRequest is answered with a single, empty batchResponse. */ |
| | | @Test |
| | | public void testEmptySoapBodyIsAnsweredWithEmptyBatchResponse() throws Exception |
| | | { |
| | | try (FakeLdapServer server = new FakeLdapServer()) |
| | | { |
| | | Map<String, String> headers = new LinkedHashMap<>(); |
| | | headers.put("Content-Type", SOAP_1_1_CONTENT_TYPE); |
| | | |
| | | String response = doPost(server.getPort(), headers, soap11("")); |
| | | |
| | | List<Element> replies = batchResponsesOf(response); |
| | | assertEquals(replies.size(), 1, response); |
| | | assertTrue(childElements(replies.get(0)).isEmpty(), response); |
| | | assertTrue(server.getReceivedOpTypes().isEmpty(), |
| | | "no connection to the directory server should have been opened"); |
| | | } |
| | | } |
| | | |
| | | /** The batchResponse elements of the reply, in document order. */ |
| | | private static List<Element> batchResponsesOf(String response) throws Exception |
| | | { |
| | | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| | | factory.setNamespaceAware(true); |
| | | Document doc = factory.newDocumentBuilder() |
| | | .parse(new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8))); |
| | | NodeList nodes = doc.getElementsByTagNameNS(DSML_NAMESPACE, "batchResponse"); |
| | | List<Element> result = new ArrayList<>(); |
| | | for (int i = 0; i < nodes.getLength(); i++) |
| | | { |
| | | result.add((Element) nodes.item(i)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private static List<Element> childElements(Element parent) |
| | | { |
| | | List<Element> result = new ArrayList<>(); |
| | | NodeList children = parent.getChildNodes(); |
| | | for (int i = 0; i < children.getLength(); i++) |
| | | { |
| | | if (children.item(i) instanceof Element) |
| | | { |
| | | result.add((Element) children.item(i)); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * Posts the given SOAP body with HTTP credentials turned into a SASL PLAIN |
| | | * authid, so that the authRequest of a batch request becomes an authzid. |
| | | */ |