diff --git a/web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java b/web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java index 0ff75ea8d72..b5ef2bd3d1f 100644 --- a/web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java +++ b/web/src/main/java/org/springframework/security/web/ObservationFilterChainDecorator.java @@ -18,7 +18,9 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -36,6 +38,36 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.security.web.access.channel.ChannelProcessingFilter; +import org.springframework.security.web.access.intercept.AuthorizationFilter; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter; +import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter; +import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.authentication.www.DigestAuthenticationFilter; +import org.springframework.security.web.context.SecurityContextHolderFilter; +import org.springframework.security.web.context.SecurityContextPersistenceFilter; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.header.HeaderWriterFilter; +import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; +import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; +import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; +import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.DisableEncodeUrlFilter; +import org.springframework.security.web.session.ForceEagerSessionCreationFilter; +import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.CorsFilter; /** * A {@link org.springframework.security.web.FilterChainProxy.FilterChainDecorator} that @@ -48,6 +80,12 @@ public final class ObservationFilterChainDecorator implements FilterChainProxy.F private static final Log logger = LogFactory.getLog(FilterChainProxy.class); + private static final int OPENTELEMETRY_MAX_NAME_LENGTH = 63; + + private static final int MAX_OBSERVATION_NAME_LENGTH = OPENTELEMETRY_MAX_NAME_LENGTH - ".before".length(); + + private static final Map OBSERVATION_NAMES = new HashMap<>(); + private static final String ATTRIBUTE = ObservationFilterChainDecorator.class + ".observation"; static final String UNSECURED_OBSERVATION_NAME = "spring.security.http.unsecured.requests"; @@ -98,10 +136,83 @@ private List wrap(List filters) { return observableFilters; } + static { + registerName(DisableEncodeUrlFilter.class, "session.encode-url.disable"); + registerName(ForceEagerSessionCreationFilter.class, "session.create"); + registerName(ChannelProcessingFilter.class, "web.request.delivery.ensure"); + registerName(WebAsyncManagerIntegrationFilter.class, "web-async-manager.join.security-context"); + registerName(SecurityContextHolderFilter.class, "security-context.hold"); + registerName(SecurityContextPersistenceFilter.class, "security-context.persist"); + registerName(HeaderWriterFilter.class, "web.response.header.set"); + registerName(CorsFilter.class, "cors.process"); + registerName(CsrfFilter.class, "csrf.protect"); + registerName(LogoutFilter.class, "principal.logout"); + registerName("org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", + "web.request.oauth2.redirect"); + registerName( + "org.springframework.security.saml2.provider.service.web." + "Saml2WebSsoAuthenticationRequestFilter", + "web.request.saml2.redirect"); + registerName(X509AuthenticationFilter.class, "web.request.x509.auth"); + registerName(AbstractPreAuthenticatedProcessingFilter.class, "web.request.pre-auth.base.process"); + registerName("org.springframework.security.cas.web.CasAuthenticationFilter", "web.request.sas.auth"); + registerName("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", + "web.response.oauth2.process"); + registerName("org.springframework.security.saml2.provider.service.web.authentication" + + ".Saml2WebSsoAuthenticationFilter", "web.request.saml2.auth"); + registerName(UsernamePasswordAuthenticationFilter.class, "web.request.username-password.auth"); + registerName(DefaultLoginPageGeneratingFilter.class, "web.login-page.default.generate"); + registerName(DefaultLogoutPageGeneratingFilter.class, "web.logout-page.default.generate"); + registerName(ConcurrentSessionFilter.class, "session.refresh"); + registerName(DigestAuthenticationFilter.class, "web.request.digest.auth"); + registerName("org.springframework.security.oauth2.server.resource.web.authentication." + + "BearerTokenAuthenticationFilter", "web.request.bearer.auth"); + registerName(BasicAuthenticationFilter.class, "web.request.basic.auth"); + registerName(RequestCacheAwareFilter.class, "web.request.cache.extract"); + registerName(SecurityContextHolderAwareRequestFilter.class, "web.request.security.wrap"); + registerName(JaasApiIntegrationFilter.class, "web.request.jass.auth"); + registerName(RememberMeAuthenticationFilter.class, "web.request.remember-me.auth"); + registerName(AnonymousAuthenticationFilter.class, "web.request.anonymous.auth"); + registerName("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", + "web.response.oauth2.code-grant.process"); + registerName(SessionManagementFilter.class, "session.manage"); + registerName(ExceptionTranslationFilter.class, "exception.translate"); + registerName(FilterSecurityInterceptor.class, "web.response.security.intercept"); + registerName(AuthorizationFilter.class, "web.access.auth.restrict"); + registerName(SwitchUserFilter.class, "session.switch"); + } + + public static void registerName(Class clazz, String name) { + String keyName = clazz.getName(); + checkAlreadyRegistered(keyName); + OBSERVATION_NAMES.put(keyName, limitLength(name)); + } + + public static void registerName(String className, String name) { + checkAlreadyRegistered(className); + OBSERVATION_NAMES.put(className, name); + } + static AroundFilterObservation observation(HttpServletRequest request) { return (AroundFilterObservation) request.getAttribute(ATTRIBUTE); } + private static String getObservationName(String className) { + if (OBSERVATION_NAMES.containsKey(className)) { + return OBSERVATION_NAMES.get(className); + } + throw new IllegalArgumentException("Class not registered for observation: " + className); + } + + private static String limitLength(String s) { + Assert.isTrue(s.length() <= MAX_OBSERVATION_NAME_LENGTH, + "The name must be less than MAX_OBSERVATION_NAME_LENGTH=" + MAX_OBSERVATION_NAME_LENGTH); + return s; + } + + private static void checkAlreadyRegistered(String keyName) { + Assert.isTrue(!OBSERVATION_NAMES.containsKey(keyName), "Observation name is registered already: " + keyName); + } + private static final class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; @@ -145,6 +256,8 @@ static final class ObservationFilter implements Filter { private final String name; + private final String observationName; + private final int position; private final int size; @@ -155,12 +268,30 @@ static final class ObservationFilter implements Filter { this.name = filter.getClass().getSimpleName(); this.position = position; this.size = size; + String tempObservationName; + try { + tempObservationName = ObservationFilterChainDecorator.getObservationName(filter.getClass().getName()); + } + catch (IllegalArgumentException ex) { + tempObservationName = compressName(this.name); + logger.warn( + "Class " + filter.getClass().getName() + + " is not registered for observation and will have name " + tempObservationName + + ". Please consider of registering this class with " + + ObservationFilterChainDecorator.class.getSimpleName() + ".registerName(class, name).", + ex); + } + this.observationName = tempObservationName; } String getName() { return this.name; } + String getObservationName() { + return this.observationName; + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -181,7 +312,8 @@ private void wrapFilter(ServletRequest request, ServletResponse response, Filter parentBefore.setFilterName(this.name); parentBefore.setChainPosition(this.position); } - parent.before().event(Observation.Event.of(this.name + ".before", "before " + this.name)); + parent.before().event(Observation.Event.of(this.observationName + ".before", + "before " + this.name)); this.filter.doFilter(request, response, chain); parent.start(); if (parent.after().getContext() instanceof FilterChainObservationContext parentAfter) { @@ -189,7 +321,8 @@ private void wrapFilter(ServletRequest request, ServletResponse response, Filter parentAfter.setFilterName(this.name); parentAfter.setChainPosition(this.size - this.position + 1); } - parent.after().event(Observation.Event.of(this.name + ".after", "after " + this.name)); + parent.after().event(Observation.Event.of(this.observationName + ".after", + "after " + this.name)); } private AroundFilterObservation parent(HttpServletRequest request) { @@ -202,6 +335,24 @@ private AroundFilterObservation parent(HttpServletRequest request) { return parent; } + private String compressName(String className) { + if (className.length() >= MAX_OBSERVATION_NAME_LENGTH) { + return maximalCompressClassName(className, MAX_OBSERVATION_NAME_LENGTH); + } + return className; + } + + private String maximalCompressClassName(String className, int maxLength) { + String[] names = className.split("(?=\\p{Lu})"); + for (int j = 0; j < names.length; j++) { + final int maxPortionLength = maxLength / names.length; + if (names[j].length() > maxPortionLength) { + names[j] = names[j].substring(0, maxPortionLength); + } + } + return StringUtils.arrayToDelimitedString(names, ""); + } + } interface AroundFilterObservation extends FilterObservation { diff --git a/web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java b/web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java index 47fe1c1b5bd..f519d11f53c 100644 --- a/web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java +++ b/web/src/test/java/org/springframework/security/web/ObservationFilterChainDecoratorTests.java @@ -16,6 +16,8 @@ package org.springframework.security.web; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.List; import io.micrometer.observation.Observation; @@ -78,6 +80,7 @@ void decorateFiltersWhenDefaultsThenObserves() throws Exception { FilterChain chain = mock(FilterChain.class); Filter filter = mock(Filter.class); FilterChain decorated = decorator.decorate(chain, List.of(filter)); + assertCompressedName(decorated); decorated.doFilter(new MockHttpServletRequest("GET", "/"), new MockHttpServletResponse()); verify(handler, times(2)).onStart(any()); ArgumentCaptor event = ArgumentCaptor.forClass(Observation.Event.class); @@ -87,4 +90,22 @@ void decorateFiltersWhenDefaultsThenObserves() throws Exception { assertThat(events.get(1).getName()).isEqualTo(filter.getClass().getSimpleName() + ".after"); } + void assertCompressedName(FilterChain filterChain) throws Exception { + assertThat(filterChain.getClass().getSimpleName()).isEqualTo("VirtualFilterChain"); + Field field = filterChain.getClass().getDeclaredField("additionalFilters"); + field.setAccessible(true); + List additionalFilters = + (List) field.get(filterChain); + assertThat(additionalFilters.size()).isEqualTo(1); + final ObservationFilterChainDecorator.ObservationFilter observationFilter = additionalFilters.get(0); + assertThat(observationFilter.getObservationName()).isEqualTo(observationFilter.getName()); + Method method = observationFilter.getClass().getDeclaredMethod("compressName", String.class); + method.setAccessible(true); + String compressed = (String) method.invoke(observationFilter, "ObservationFilterChainDecoratorTests"); + assertThat(compressed).isEqualTo("ObservationFilterChainDecoratorTests"); + String fakeCompressed = (String) method.invoke(observationFilter, + "ObservationFilterChainDecoratorTestsObservationFilterChainDecoratorTests"); + assertThat(fakeCompressed).isEqualTo("ObserFilteChainDecorTestsObserFilteChainDecorTests"); + } + }