blob: 861a88690445547d57d12979ed975ba8ad772438 [file] [log] [blame]
/*
* 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.knox.gateway;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.knox.gateway.audit.api.Action;
import org.apache.knox.gateway.audit.api.ActionOutcome;
import org.apache.knox.gateway.audit.api.AuditContext;
import org.apache.knox.gateway.audit.api.AuditServiceFactory;
import org.apache.knox.gateway.audit.api.CorrelationContext;
import org.apache.knox.gateway.audit.api.ResourceType;
import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
import org.apache.knox.gateway.audit.log4j.audit.Log4jAuditService;
import org.apache.knox.gateway.audit.log4j.correlation.Log4jCorrelationService;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.dispatch.DefaultDispatch;
import org.apache.knox.test.log.CollectAppender;
import org.apache.log4j.spi.LoggingEvent;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.fail;
public class AuditLoggingTest {
private static Logger LOG = LoggerFactory.getLogger( AuditLoggingTest.class );
private static final String METHOD = "GET";
private static final String PATH = "path";
private static final String CONTEXT_PATH = "contextPath/";
private static final String ADDRESS = "address";
private static final String HOST = "host";
@Before
public void setUp() {
AuditServiceFactory.getAuditService().createContext();
CollectAppender.queue.clear();
}
@After
public void tearDown() {
AuditServiceFactory.getAuditService().detachContext();
}
/*
* Empty filter chain. Two events with same correlation ID are expected:
*
* action=access request_type=uri outcome=unavailable
* action=access request_type=uri outcome=success message=Response status: 404
*/
@Test
public void testNoFiltersAudit() throws Exception {
FilterConfig config = EasyMock.createNiceMock( FilterConfig.class );
EasyMock.replay( config );
HttpServletRequest request = EasyMock.createNiceMock( HttpServletRequest.class );
ServletContext context = EasyMock.createNiceMock( ServletContext.class );
GatewayConfig gatewayConfig = EasyMock.createNiceMock( GatewayConfig.class );
EasyMock.expect( request.getMethod() ).andReturn( METHOD ).anyTimes();
EasyMock.expect( request.getPathInfo() ).andReturn( PATH ).anyTimes();
EasyMock.expect( request.getContextPath() ).andReturn( CONTEXT_PATH ).anyTimes();
EasyMock.expect( request.getRemoteAddr() ).andReturn( ADDRESS ).anyTimes();
EasyMock.expect( request.getRemoteHost() ).andReturn( HOST ).anyTimes();
EasyMock.expect( request.getServletContext() ).andReturn( context ).anyTimes();
EasyMock.expect( context.getAttribute(
GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(gatewayConfig).anyTimes();
EasyMock.expect(gatewayConfig.getHeaderNameForRemoteAddress()).andReturn(
"Custom-Forwarded-For").anyTimes();
EasyMock.replay( request );
EasyMock.replay( context );
EasyMock.replay( gatewayConfig );
HttpServletResponse response = EasyMock.createNiceMock( HttpServletResponse.class );
EasyMock.replay( response );
FilterChain chain = EasyMock.createNiceMock( FilterChain.class );
EasyMock.replay( chain );
Random rnd = new Random();
// Make number of total requests between 1-100
int numberTotalRequests = rnd.nextInt(99) + 1;
Set<Callable<Void>> callables = new HashSet<>(numberTotalRequests);
for (int i = 0; i < numberTotalRequests; i++) {
callables.add(() -> {
GatewayFilter gateway = new GatewayFilter();
gateway.init( config );
gateway.doFilter( request, response, chain );
gateway.destroy();
return null;
});
}
// Make number of concurrent requests between 1-10
int numberConcurrentRequests = rnd.nextInt( 9) + 1;
LOG.info("Executing %d total requests with %d concurrently", numberTotalRequests, numberConcurrentRequests);
ExecutorService executor = Executors.newFixedThreadPool(numberConcurrentRequests);
executor.invokeAll(callables);
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
assertThat(executor.isTerminated(), is(true));
assertThat( CollectAppender.queue.size(), is( numberTotalRequests ) );
// Use a set to make sure to dedupe any requestIds to get only unique ones
Set<String> requestIds = new HashSet<>();
for (LoggingEvent accessEvent : CollectAppender.queue) {
verifyAuditEvent( accessEvent, CONTEXT_PATH + PATH, ResourceType.URI, Action.ACCESS, ActionOutcome.UNAVAILABLE, null, "Request method: GET" );
CorrelationContext cc = (CorrelationContext)accessEvent.getMDC( Log4jCorrelationService.MDC_CORRELATION_CONTEXT_KEY );
// There are some events that do not have a CorrelationContext associated (ie: deploy)
if(cc != null) {
requestIds.add(cc.getRequestId());
}
}
// There should be a unique correlation id for each request
assertThat(requestIds.size(), is(numberTotalRequests));
}
/*
* One NoOp filter in chain. Single audit event with same with specified request URI is expected:
*
* action=access request_type=uri outcome=unavailable
*/
@Test
public void testNoopFilter() throws ServletException, IOException,
URISyntaxException {
FilterConfig config = EasyMock.createNiceMock( FilterConfig.class );
EasyMock.replay( config );
HttpServletRequest request = EasyMock.createNiceMock( HttpServletRequest.class );
ServletContext context = EasyMock.createNiceMock( ServletContext.class );
GatewayConfig gatewayConfig = EasyMock.createNiceMock( GatewayConfig.class );
EasyMock.expect( request.getMethod() ).andReturn( METHOD ).anyTimes();
EasyMock.expect( request.getPathInfo() ).andReturn( PATH ).anyTimes();
EasyMock.expect( request.getContextPath() ).andReturn( CONTEXT_PATH ).anyTimes();
EasyMock.expect( request.getRemoteAddr() ).andReturn( ADDRESS ).anyTimes();
EasyMock.expect( request.getRemoteHost() ).andReturn( HOST ).anyTimes();
EasyMock.expect( request.getServletContext() ).andReturn( context ).anyTimes();
EasyMock.expect( context.getAttribute(
GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(gatewayConfig).anyTimes();
EasyMock.expect(gatewayConfig.getHeaderNameForRemoteAddress()).andReturn(
"Custom-Forwarded-For").anyTimes();
EasyMock.replay( request );
EasyMock.replay( context );
EasyMock.replay( gatewayConfig );
HttpServletResponse response = EasyMock.createNiceMock( HttpServletResponse.class );
EasyMock.replay( response );
FilterChain chain = EasyMock.createNiceMock( FilterChain.class );
EasyMock.replay( chain );
Filter filter = EasyMock.createNiceMock( Filter.class );
EasyMock.replay( filter );
GatewayFilter gateway = new GatewayFilter();
gateway.addFilter( "path", "filter", filter, null, null );
gateway.init( config );
gateway.doFilter( request, response, chain );
gateway.destroy();
assertThat( CollectAppender.queue.size(), is( 1 ) );
Iterator<LoggingEvent> iterator = CollectAppender.queue.iterator();
LoggingEvent accessEvent = iterator.next();
verifyAuditEvent( accessEvent, CONTEXT_PATH + PATH, ResourceType.URI,
Action.ACCESS, ActionOutcome.UNAVAILABLE, null, "Request method: GET" );
}
/*
* Dispatching outbound request. Remote host is unreachable. Two log events is expected:
*
* action=dispatch request_type=uri outcome=FAILED
* action=dispatch request_type=uri outcome=unavailable
*/
@Test
public void testHttpClientOutboundException() throws IOException,
URISyntaxException {
String uri = "http://outbound-host.invalid:port/path";
HttpServletRequest inboundRequest = EasyMock.createNiceMock( HttpServletRequest.class );
EasyMock.expect( inboundRequest.getHeaderNames() ).andReturn( Collections.enumeration(new ArrayList<>() ) ).anyTimes();
EasyMock.replay( inboundRequest );
HttpServletResponse outboundResponse = EasyMock.createNiceMock( HttpServletResponse.class );
EasyMock.replay( outboundResponse );
DefaultDispatch dispatch = new DefaultDispatch();
HttpClientBuilder builder = HttpClientBuilder.create();
CloseableHttpClient client = builder.build();
dispatch.setHttpClient(client);
try {
dispatch.doGet( new URI( uri ), inboundRequest, outboundResponse );
fail( "Expected exception while accessing to unreachable host" );
} catch ( IOException e ) {
Iterator<LoggingEvent> iterator = CollectAppender.queue.iterator();
LoggingEvent unavailableEvent = iterator.next();
verifyValue( (String) unavailableEvent.getMDC( AuditConstants.MDC_RESOURCE_NAME_KEY ), uri );
verifyValue( (String) unavailableEvent.getMDC( AuditConstants.MDC_RESOURCE_TYPE_KEY ), ResourceType.URI );
verifyValue( (String) unavailableEvent.getMDC( AuditConstants.MDC_ACTION_KEY ), Action.DISPATCH );
verifyValue( (String) unavailableEvent.getMDC( AuditConstants.MDC_OUTCOME_KEY ), ActionOutcome.UNAVAILABLE );
LoggingEvent failureEvent = iterator.next();
verifyValue( (String) failureEvent.getMDC( AuditConstants.MDC_RESOURCE_NAME_KEY ), uri );
verifyValue( (String) failureEvent.getMDC( AuditConstants.MDC_RESOURCE_TYPE_KEY ), ResourceType.URI );
verifyValue( (String) failureEvent.getMDC( AuditConstants.MDC_ACTION_KEY ), Action.DISPATCH );
verifyValue( (String) failureEvent.getMDC( AuditConstants.MDC_OUTCOME_KEY ), ActionOutcome.FAILURE );
}
}
private void verifyAuditEvent( LoggingEvent event, String resourceName,
String resourceType, String action, String outcome, String targetService,
String message ) {
event.getMDCCopy();
CorrelationContext cc = (CorrelationContext) event.getMDC( Log4jCorrelationService.MDC_CORRELATION_CONTEXT_KEY );
assertThat( cc, notNullValue() );
assertThat( cc.getRequestId(), is( notNullValue() ) );
AuditContext ac = (AuditContext) event.getMDC( Log4jAuditService.MDC_AUDIT_CONTEXT_KEY );
assertThat( ac, notNullValue() );
assertThat( ac.getRemoteIp(), is( ADDRESS ) );
assertThat( ac.getRemoteHostname(), is( HOST ) );
assertThat(event.getMDC( AuditConstants.MDC_SERVICE_KEY ), is( AuditConstants.KNOX_SERVICE_NAME ) );
assertThat(event.getMDC( AuditConstants.MDC_COMPONENT_KEY ), is( AuditConstants.KNOX_COMPONENT_NAME ) );
assertThat(event.getLoggerName(), is( AuditConstants.DEFAULT_AUDITOR_NAME ) );
verifyValue( (String) event.getMDC( AuditConstants.MDC_RESOURCE_NAME_KEY ), resourceName );
verifyValue( (String) event.getMDC( AuditConstants.MDC_RESOURCE_TYPE_KEY ), resourceType );
verifyValue( (String) event.getMDC( AuditConstants.MDC_ACTION_KEY ), action );
verifyValue( (String) event.getMDC( AuditConstants.MDC_OUTCOME_KEY ), outcome );
verifyValue( ac.getTargetServiceName(), targetService );
verifyValue( event.getRenderedMessage(), message );
}
private void verifyValue( String actual, String expected ) {
if( expected == null ) {
assertThat( actual, nullValue() );
} else {
assertThat( actual, is( expected ) );
}
}
}