NIFI-7023 This closes #3991. Removed SLF4J and Log4J transitive dependencies from Zookeeper so tests log correctly.

Changed template handling.
Added unit tests.

Signed-off-by: Joe Witt <joewitt@apache.org>
This commit is contained in:
Andy LoPresto 2020-01-15 19:16:57 -08:00 committed by Joe Witt
parent ce050f4ecb
commit 34f2a592df
No known key found for this signature in database
GPG Key ID: 9093BF854F811A1A
3 changed files with 254 additions and 65 deletions

View File

@ -161,6 +161,16 @@
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>

View File

@ -24,7 +24,58 @@ import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.annotations.Authorization;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLStreamReader;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.nifi.authorization.AuthorizableLookup;
import org.apache.nifi.authorization.AuthorizeAccess;
import org.apache.nifi.authorization.AuthorizeControllerServiceReference;
@ -120,57 +171,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* RESTful endpoint for managing a Group.
*/
@ -1100,18 +1100,10 @@ public class ProcessGroupResource extends ApplicationResource {
return false;
}
if (desiredState == ScheduledState.STOPPED && status.getAggregateSnapshot().getActiveThreadCount() != 0) {
return false;
}
return true;
return desiredState != ScheduledState.STOPPED || status.getAggregateSnapshot().getActiveThreadCount() == 0;
});
if (!allProcessorsMatch) {
return false;
}
return true;
return allProcessorsMatch;
}
/**
@ -3631,6 +3623,7 @@ public class ProcessGroupResource extends ApplicationResource {
// unmarshal the template
final TemplateDTO template;
try {
// TODO: Potentially refactor the template parsing to a service layer outside of the resource for web request handling
JAXBContext context = JAXBContext.newInstance(TemplateDTO.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
XMLStreamReader xsr = XmlUtils.createSafeReader(in);
@ -3642,12 +3635,12 @@ public class ProcessGroupResource extends ApplicationResource {
return Response.status(Response.Status.OK).entity(responseXml).type("application/xml").build();
} catch (IllegalArgumentException iae) {
logger.warn("Unable to import template.", iae);
String responseXml = String.format("<errorResponse status=\"%s\" statusText=\"%s\"/>", Response.Status.BAD_REQUEST.getStatusCode(), iae.getMessage());
String responseXml = String.format("<errorResponse status=\"%s\" statusText=\"%s\"/>", Response.Status.BAD_REQUEST.getStatusCode(), sanitizeErrorResponse(iae.getMessage()));
return Response.status(Response.Status.OK).entity(responseXml).type("application/xml").build();
} catch (Exception e) {
logger.warn("An error occurred while importing a template.", e);
String responseXml = String.format("<errorResponse status=\"%s\" statusText=\"Unable to import the specified template: %s\"/>",
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e.getMessage());
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), sanitizeErrorResponse(e.getMessage()));
return Response.status(Response.Status.OK).entity(responseXml).type("application/xml").build();
}
@ -3683,6 +3676,20 @@ public class ProcessGroupResource extends ApplicationResource {
return importTemplate(httpServletRequest, groupId, entity);
}
/**
* Returns the sanitized error response which can safely be displayed on the error page.
*
* @param errorResponse the initial error response
* @return the HTML-escaped error response
*/
private String sanitizeErrorResponse(String errorResponse) {
if (errorResponse == null || StringUtils.isEmpty(errorResponse)) {
return "";
}
return StringEscapeUtils.escapeHtml4(errorResponse);
}
/**
* Imports the specified template.
*
@ -3751,12 +3758,12 @@ public class ProcessGroupResource extends ApplicationResource {
return generateCreatedResponse(URI.create(template.getUri()), entity).build();
} catch (IllegalArgumentException | IllegalStateException e) {
logger.info("Unable to import template: " + e);
String responseXml = String.format("<errorResponse status=\"%s\" statusText=\"%s\"/>", Response.Status.BAD_REQUEST.getStatusCode(), e.getMessage());
String responseXml = String.format("<errorResponse status=\"%s\" statusText=\"%s\"/>", Response.Status.BAD_REQUEST.getStatusCode(), sanitizeErrorResponse(e.getMessage()));
return Response.status(Response.Status.OK).entity(responseXml).type("application/xml").build();
} catch (Exception e) {
logger.warn("An error occurred while importing a template.", e);
String responseXml = String.format("<errorResponse status=\"%s\" statusText=\"Unable to import the specified template: %s\"/>",
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), e.getMessage());
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), sanitizeErrorResponse(e.getMessage()));
return Response.status(Response.Status.OK).entity(responseXml).type("application/xml").build();
}
}

View File

@ -0,0 +1,172 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.api
import org.apache.nifi.authorization.AuthorizeAccess
import org.apache.nifi.util.NiFiProperties
import org.apache.nifi.web.NiFiServiceFacade
import org.apache.nifi.web.api.dto.FlowSnippetDTO
import org.apache.nifi.web.api.dto.TemplateDTO
import org.apache.nifi.web.api.entity.TemplateEntity
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.Response
import javax.ws.rs.core.UriInfo
@RunWith(JUnit4.class)
class ProcessGroupResourceTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(ProcessGroupResourceTest.class)
@Rule
public TestName testName = new TestName()
@BeforeClass
static void setUpOnce() throws Exception {
logger.metaClass.methodMissing = { String name, args ->
logger.debug("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
void setUp() throws Exception {
}
@After
void tearDown() throws Exception {
}
/** This test creates a malformed template upload request to exercise error handling and sanitization */
@Test
void testUploadShouldHandleMalformedTemplate() {
// Arrange
ProcessGroupResource pgResource = new ProcessGroupResource()
// Mocking the returned template object to throw a specific exception would be nice
final String TEMPLATE_WITH_XSS_PLAIN = "<?xml version=\"1.0\" encoding='><script xmlns=\"http://www.w3.org/1999/xhtml\">alert(JSON.stringify(localstorage));</script><errorResponse test='?>"
logger.info("Malformed template XML: ${TEMPLATE_WITH_XSS_PLAIN}")
InputStream contentInputStream = new ByteArrayInputStream(TEMPLATE_WITH_XSS_PLAIN.bytes)
HttpServletRequest mockRequest = [:] as HttpServletRequest
UriInfo mockUriInfo = [:] as UriInfo
String groupId = "1"
// Build a malformed template object which can be unmarshalled from XML
// Act
// Try to submit the malformed template
Response response = pgResource.uploadTemplate(mockRequest, mockUriInfo, groupId, false, contentInputStream)
logger.info("Response: ${response}")
// Assert
// Assert that the expected error response was returned
assert response.status == Response.Status.OK.statusCode
// Assert that the error response is sanitized
String responseEntity = response.entity as String
logger.info("Error response: ${responseEntity}")
assert !(responseEntity =~ /<script.*>/)
}
/** This test creates a malformed template import request to exercise error handling and sanitization */
@Test
void testImportShouldHandleMalformedTemplate() {
// Arrange
ProcessGroupResource pgResource = new ProcessGroupResource()
// Configure parent fields for write lock process
pgResource.properties = [isNode: { -> return false }] as NiFiProperties
pgResource.serviceFacade = [
authorizeAccess : { AuthorizeAccess a -> },
verifyCanAddTemplate: { String gid, String templateName -> },
importTemplate : { TemplateDTO template, String gid, Optional<String> seedId ->
logger.mock("Called importTemplate;")
template
}
] as NiFiServiceFacade
pgResource.templateResource = [
populateRemainingTemplateContent: { TemplateDTO td -> }
] as TemplateResource
final String TEMPLATE_WITH_XSS_PLAIN = "<?xml version=\"1.0\" encoding='><script xmlns=\"http://www.w3.org/1999/xhtml\">alert(JSON.stringify(localstorage));</script><errorResponse test='?>"
logger.info("Malformed template XML: ${TEMPLATE_WITH_XSS_PLAIN}")
TemplateDTO mockIAETemplate = [
getName : { -> "mockIAETemplate" },
getUri : { ->
throw new IllegalArgumentException("Expected exception with <script> element")
},
getSnippet: { -> new FlowSnippetDTO() }
] as TemplateDTO
TemplateDTO mockExceptionTemplate = [
getName : { -> "mockExceptionTemplate" },
getUri : { ->
throw new RuntimeException("Expected exception with <script> element")
},
getSnippet: { -> new FlowSnippetDTO() }
] as TemplateDTO
TemplateEntity mockIAETemplateEntity = [getTemplate: { ->
mockIAETemplate
}] as TemplateEntity
TemplateEntity mockExceptionTemplateEntity = [getTemplate: { ->
mockExceptionTemplate
}] as TemplateEntity
// Override the request object and store it for ApplicationResource#withWriteLock
HttpServletRequest mockRequest = [getHeader: { String headerName ->
logger.mock("Requesting header ${headerName}; returning null")
null
}] as HttpServletRequest
// Set the persisted request object so the parent ApplicationResource can use it
pgResource.httpServletRequest = mockRequest
String groupId = "1"
// Act
List<Response> responses = [mockIAETemplateEntity, mockExceptionTemplateEntity].collect { TemplateEntity te ->
// Try to submit the malformed template which throws some kind of exception
Response response = pgResource.importTemplate(mockRequest, groupId, te)
logger.info("Response: ${response}")
response
}
// Assert
responses.each { Response r ->
// Assert that the expected error response was returned
assert r.status == Response.Status.OK.statusCode
// Assert that the error response is sanitized
String entity = r.entity as String
logger.info("Error response: ${entity}")
assert !(entity =~ /<script.*>/)
}
}
}