| /* |
| * 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 ) ); |
| } |
| } |
| } |