Instrument Filter Chain

Closes gh-11911
This commit is contained in:
Josh Cummings
2022-09-21 18:04:31 -06:00
parent 8c610684f3
commit 99a87179dd
12 changed files with 1798 additions and 21 deletions

View File

@@ -16,19 +16,27 @@
package org.springframework.security.web;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -50,7 +58,9 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -270,10 +280,198 @@ public class FilterChainProxyTests {
this.fcp.setRequestRejectedHandler(rjh);
RequestRejectedException requestRejectedException = new RequestRejectedException("Contains illegal chars");
ServletException servletException = new ServletException(requestRejectedException);
given(fw.getFirewalledRequest(this.request)).willReturn(mock(FirewalledRequest.class));
given(fw.getFirewalledRequest(this.request)).willReturn(new MockFirewalledRequest(this.request));
willThrow(servletException).given(this.chain).doFilter(any(), any());
this.fcp.doFilter(this.request, this.response, this.chain);
verify(rjh).handle(eq(this.request), eq(this.response), eq((requestRejectedException)));
}
@Test
public void doFilterWhenMatchesThenObservationRegistryObserves() throws Exception {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
given(this.matcher.matches(any())).willReturn(true);
SecurityFilterChain sec = new DefaultSecurityFilterChain(this.matcher, Arrays.asList(this.filter));
FilterChainProxy fcp = new FilterChainProxy(sec);
fcp.setFilterChainDecorator(new ObservationFilterChainDecorator(registry));
Filter filter = ObservationFilterChainDecorator.FilterObservation
.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
filter.doFilter(this.request, this.response, this.chain);
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(4)).onStart(captor.capture());
verify(handler, times(4)).onStop(any());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertFilterChainObservation(contexts.next(), "before", 1);
assertThat(contexts.next().getName()).isEqualTo(ObservationFilterChainDecorator.SECURED_OBSERVATION_NAME);
assertFilterChainObservation(contexts.next(), "after", 1);
}
@Test
public void doFilterWhenMultipleFiltersThenObservationRegistryObserves() throws Exception {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
given(this.matcher.matches(any())).willReturn(true);
Filter one = mockFilter();
Filter two = mockFilter();
Filter three = mockFilter();
SecurityFilterChain sec = new DefaultSecurityFilterChain(this.matcher, one, two, three);
FilterChainProxy fcp = new FilterChainProxy(sec);
fcp.setFilterChainDecorator(new ObservationFilterChainDecorator(registry));
Filter filter = ObservationFilterChainDecorator.FilterObservation
.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
filter.doFilter(this.request, this.response, this.chain);
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(4)).onStart(captor.capture());
verify(handler, times(4)).onStop(any());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertFilterChainObservation(contexts.next(), "before", 3);
assertThat(contexts.next().getName()).isEqualTo(ObservationFilterChainDecorator.SECURED_OBSERVATION_NAME);
assertFilterChainObservation(contexts.next(), "after", 3);
}
@Test
public void doFilterWhenMismatchesThenObservationRegistryObserves() throws Exception {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
SecurityFilterChain sec = new DefaultSecurityFilterChain(this.matcher, Arrays.asList(this.filter));
FilterChainProxy fcp = new FilterChainProxy(sec);
fcp.setFilterChainDecorator(new ObservationFilterChainDecorator(registry));
Filter filter = ObservationFilterChainDecorator.FilterObservation
.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
filter.doFilter(this.request, this.response, this.chain);
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(2)).onStart(captor.capture());
verify(handler, times(2)).onStop(any());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertThat(contexts.next().getName()).isEqualTo(ObservationFilterChainDecorator.UNSECURED_OBSERVATION_NAME);
}
@Test
public void doFilterWhenFilterExceptionThenObservationRegistryObserves() throws Exception {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
willThrow(IllegalStateException.class).given(this.filter).doFilter(any(), any(), any());
given(this.matcher.matches(any())).willReturn(true);
SecurityFilterChain sec = new DefaultSecurityFilterChain(this.matcher, Arrays.asList(this.filter));
FilterChainProxy fcp = new FilterChainProxy(sec);
fcp.setFilterChainDecorator(new ObservationFilterChainDecorator(registry));
Filter filter = ObservationFilterChainDecorator.FilterObservation
.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> filter.doFilter(this.request, this.response, this.chain));
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(2)).onStart(captor.capture());
verify(handler, times(2)).onStop(any());
verify(handler, atLeastOnce()).onError(any());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertFilterChainObservation(contexts.next(), "before", 1);
}
@Test
public void doFilterWhenExceptionWithMultipleFiltersThenObservationRegistryObserves() throws Exception {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
given(this.matcher.matches(any())).willReturn(true);
Filter one = mockFilter();
Filter two = mock(Filter.class);
willThrow(IllegalStateException.class).given(two).doFilter(any(), any(), any());
Filter three = mockFilter();
SecurityFilterChain sec = new DefaultSecurityFilterChain(this.matcher, one, two, three);
FilterChainProxy fcp = new FilterChainProxy(sec);
fcp.setFilterChainDecorator(new ObservationFilterChainDecorator(registry));
Filter filter = ObservationFilterChainDecorator.FilterObservation
.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(() -> filter.doFilter(this.request, this.response, this.chain));
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(2)).onStart(captor.capture());
verify(handler, times(2)).onStop(any());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertFilterChainObservation(contexts.next(), "before", 2);
}
@Test
public void doFilterWhenOneFilterDoesNotProceedThenObservationRegistryObserves() throws Exception {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
given(this.matcher.matches(any())).willReturn(true);
Filter one = mockFilter();
Filter two = mock(Filter.class);
Filter three = mockFilter();
SecurityFilterChain sec = new DefaultSecurityFilterChain(this.matcher, one, two, three);
FilterChainProxy fcp = new FilterChainProxy(sec);
fcp.setFilterChainDecorator(new ObservationFilterChainDecorator(registry));
Filter filter = ObservationFilterChainDecorator.FilterObservation
.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
filter.doFilter(this.request, this.response, this.chain);
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(3)).onStart(captor.capture());
verify(handler, times(3)).onStop(any());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertFilterChainObservation(contexts.next(), "before", 2);
assertFilterChainObservation(contexts.next(), "after", 3);
}
static void assertFilterChainObservation(Observation.Context context, String filterSection, int chainPosition) {
assertThat(context).isInstanceOf(ObservationFilterChainDecorator.FilterChainObservationContext.class);
ObservationFilterChainDecorator.FilterChainObservationContext filterChainObservationContext = (ObservationFilterChainDecorator.FilterChainObservationContext) context;
assertThat(context.getName())
.isEqualTo(ObservationFilterChainDecorator.FilterChainObservationConvention.CHAIN_OBSERVATION_NAME);
assertThat(context.getContextualName()).endsWith(filterSection);
assertThat(filterChainObservationContext.getChainPosition()).isEqualTo(chainPosition);
}
static Filter mockFilter() throws Exception {
Filter filter = mock(Filter.class);
willAnswer((invocation) -> {
HttpServletRequest request = invocation.getArgument(0, HttpServletRequest.class);
HttpServletResponse response = invocation.getArgument(1, HttpServletResponse.class);
FilterChain chain = invocation.getArgument(2, FilterChain.class);
chain.doFilter(request, response);
return null;
}).given(filter).doFilter(any(), any(), any());
return filter;
}
private static class MockFirewalledRequest extends FirewalledRequest {
MockFirewalledRequest(HttpServletRequest request) {
super(request);
}
@Override
public void reset() {
}
}
private static class MockFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
}
}
}

View File

@@ -17,12 +17,22 @@
package org.springframework.security.web.server;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.web.server.ObservationWebFilterChainDecorator.WebFilterChainObservationContext;
import org.springframework.security.web.server.ObservationWebFilterChainDecorator.WebFilterChainObservationConvention;
import org.springframework.security.web.server.ObservationWebFilterChainDecorator.WebFilterObservation;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
import org.springframework.test.web.reactive.server.WebTestClient;
@@ -30,6 +40,15 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Rob Winch
* @since 5.0
@@ -47,6 +66,86 @@ public class WebFilterChainProxyTests {
.isNotFound();
}
@Test
public void doFilterWhenMatchesThenObservationRegistryObserves() {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
List<WebFilter> filters = Arrays.asList(new PassthroughWebFilter());
ServerWebExchangeMatcher match = (exchange) -> MatchResult.match();
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(match, filters);
WebFilterChainProxy fcp = new WebFilterChainProxy(chain);
fcp.setFilterChainDecorator(new ObservationWebFilterChainDecorator(registry));
WebFilter filter = WebFilterObservation.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
WebFilterChain mockChain = mock(WebFilterChain.class);
given(mockChain.filter(any())).willReturn(Mono.empty());
filter.filter(MockServerWebExchange.from(MockServerHttpRequest.get("/")), mockChain).block();
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(4)).onStart(captor.capture());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertFilterChainObservation(contexts.next(), "before", 1);
assertThat(contexts.next().getName()).isEqualTo(ObservationWebFilterChainDecorator.SECURED_OBSERVATION_NAME);
assertFilterChainObservation(contexts.next(), "after", 1);
}
@Test
public void doFilterWhenMismatchesThenObservationRegistryObserves() {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
List<WebFilter> filters = Arrays.asList(new PassthroughWebFilter());
ServerWebExchangeMatcher notMatch = (exchange) -> MatchResult.notMatch();
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(notMatch, filters);
WebFilterChainProxy fcp = new WebFilterChainProxy(chain);
fcp.setFilterChainDecorator(new ObservationWebFilterChainDecorator(registry));
WebFilter filter = WebFilterObservation.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
WebFilterChain mockChain = mock(WebFilterChain.class);
given(mockChain.filter(any())).willReturn(Mono.empty());
filter.filter(MockServerWebExchange.from(MockServerHttpRequest.get("/")), mockChain).block();
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(2)).onStart(captor.capture());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertThat(contexts.next().getName()).isEqualTo(ObservationWebFilterChainDecorator.UNSECURED_OBSERVATION_NAME);
}
@Test
public void doFilterWhenFilterExceptionThenObservationRegistryObserves() {
ObservationHandler<Observation.Context> handler = mock(ObservationHandler.class);
given(handler.supportsContext(any())).willReturn(true);
ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig().observationHandler(handler);
WebFilter error = mock(WebFilter.class);
given(error.filter(any(), any())).willReturn(Mono.error(new IllegalStateException()));
List<WebFilter> filters = Arrays.asList(error);
ServerWebExchangeMatcher match = (exchange) -> MatchResult.match();
MatcherSecurityWebFilterChain chain = new MatcherSecurityWebFilterChain(match, filters);
WebFilterChainProxy fcp = new WebFilterChainProxy(chain);
fcp.setFilterChainDecorator(new ObservationWebFilterChainDecorator(registry));
WebFilter filter = WebFilterObservation.create(Observation.createNotStarted("wrap", registry)).wrap(fcp);
WebFilterChain mockChain = mock(WebFilterChain.class);
given(mockChain.filter(any())).willReturn(Mono.empty());
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(
() -> filter.filter(MockServerWebExchange.from(MockServerHttpRequest.get("/")), mockChain).block());
ArgumentCaptor<Observation.Context> captor = ArgumentCaptor.forClass(Observation.Context.class);
verify(handler, times(2)).onStart(captor.capture());
verify(handler, atLeastOnce()).onError(any());
Iterator<Observation.Context> contexts = captor.getAllValues().iterator();
assertThat(contexts.next().getName()).isEqualTo("wrap");
assertFilterChainObservation(contexts.next(), "before", 1);
}
static void assertFilterChainObservation(Observation.Context context, String filterSection, int chainPosition) {
assertThat(context).isInstanceOf(WebFilterChainObservationContext.class);
WebFilterChainObservationContext filterChainObservationContext = (WebFilterChainObservationContext) context;
assertThat(context.getName()).isEqualTo(WebFilterChainObservationConvention.CHAIN_OBSERVATION_NAME);
assertThat(context.getContextualName()).endsWith(filterSection);
assertThat(filterChainObservationContext.getChainPosition()).isEqualTo(chainPosition);
}
static class Http200WebFilter implements WebFilter {
@Override
@@ -56,4 +155,13 @@ public class WebFilterChainProxyTests {
}
static class PassthroughWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange);
}
}
}