sboorlagadda commented on code in PR #7940:
URL: https://github.com/apache/geode/pull/7940#discussion_r2442437552


##########
TESTING.md:
##########
@@ -13,8 +13,10 @@ Tests are broken up into five types - unit, integration, 
distributed, acceptance
   `./gradlew distributedTest`
 * Acceptance tests: test Geode from end user perspective  
   `./gradlew acceptanceTest`
-* Upgrade tests: test compatibility between versions of Geode and rolling 
upgrades  
+* Upgrade tests: test backwards compatibility and rolling upgrades between 
versions of Geode  
   `./gradlew upgradeTest`
+  
+  **Note**: Rolling upgrades are **NOT supported** across the Jakarta EE 10 
migration boundary (pre-migration → post-migration) for Tomcat session 
replication due to the javax.servlet → jakarta.servlet API incompatibility. 
Rolling upgrades within the same API era continue to work.

Review Comment:
   How are we planning to document this breaking change for our users? May be 
we should document a clear migration path by adding
   
        1. Specific version requirements for Tomcat 10.1+ vs 11.x
        2. Clear timeline for when Tomcat 6/7/8/9 support will be completely 
removed



##########
geode-web-management/src/main/java/org/apache/geode/management/internal/rest/security/RestSecurityConfiguration.java:
##########
@@ -16,98 +16,351 @@
 
 
 import java.io.IOException;
-import java.util.Arrays;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.MediaType;
 import org.springframework.security.authentication.AuthenticationManager;
-import 
org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
-import 
org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.authentication.ProviderManager;
+import 
org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
 import 
org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import 
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import 
org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.SecurityFilterChain;
 import 
org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.web.multipart.MultipartResolver;
-import org.springframework.web.multipart.commons.CommonsMultipartResolver;
+import 
org.springframework.web.multipart.support.StandardServletMultipartResolver;
 
 import org.apache.geode.management.api.ClusterManagementResult;
-import org.apache.geode.management.configuration.Links;
 
+/**
+ * Spring Security 6.x migration changes:
+ *
+ * <p>
+ * <b>Architecture Changes:</b>
+ * </p>
+ * <ul>
+ * <li>WebSecurityConfigurerAdapter → Component-based configuration (adapter 
deprecated in Spring
+ * Security 5.7, removed in 6.0)</li>
+ * <li>Override methods → Bean-based SecurityFilterChain configuration</li>
+ * <li>ProviderManager constructor replaces AuthenticationManagerBuilder 
pattern</li>
+ * </ul>
+ *
+ * <p>
+ * <b>API Modernization:</b>
+ * </p>
+ * <ul>
+ * <li>@EnableGlobalMethodSecurity → @EnableMethodSecurity (new annotation 
name)</li>
+ * <li>antMatchers() → requestMatchers() with AntPathRequestMatcher 
(deprecated method removed)</li>
+ * <li>Method chaining (.and()) → Lambda DSL configuration (modern fluent 
API)</li>
+ * <li>authorizeRequests() → authorizeHttpRequests() (new method name)</li>
+ * </ul>
+ *
+ * <p>
+ * <b>Multipart Resolver:</b>
+ * </p>
+ * <ul>
+ * <li>CommonsMultipartResolver → StandardServletMultipartResolver</li>
+ * <li>Reason: Spring 6.x standardized on Servlet 3.0+ native multipart 
support</li>
+ * <li>Note: Custom isMultipart() logic removed - 
StandardServletMultipartResolver handles PUT/POST
+ * automatically</li>
+ * </ul>
+ *
+ * <p>
+ * <b>JWT Authentication Failure Handler:</b>
+ * </p>
+ * <ul>
+ * <li>Added explicit error response handling in 
authenticationFailureHandler</li>
+ * <li>Returns proper HTTP 401 with JSON ClusterManagementResult for 
UNAUTHENTICATED status</li>
+ * <li>Previously relied on default behavior; now explicitly defined for 
clarity</li>
+ * </ul>
+ *
+ * <p>
+ * <b>Security Filter Chain:</b>
+ * </p>
+ * <ul>
+ * <li>configure(HttpSecurity) → filterChain(HttpSecurity) returning 
SecurityFilterChain</li>
+ * <li>SecurityFilterChain bean is Spring Security 6.x's recommended 
approach</li>
+ * <li>setAuthenticationManager() explicitly called on JwtAuthenticationFilter 
(required in
+ * 6.x)</li>
+ * </ul>
+ */
 @Configuration
 @EnableWebSecurity
-@EnableGlobalMethodSecurity(prePostEnabled = true)
+@EnableMethodSecurity(prePostEnabled = true)
 // this package name needs to be different than the admin rest controller's 
package name
 // otherwise this component scan will pick up the admin rest controllers as 
well.
-@ComponentScan("org.apache.geode.management.internal.rest")
-public class RestSecurityConfiguration extends WebSecurityConfigurerAdapter {
+@ComponentScan(basePackages = "org.apache.geode.management.internal.rest")
+public class RestSecurityConfiguration {
 
   @Autowired
   private GeodeAuthenticationProvider authProvider;
 
   @Autowired
   private ObjectMapper objectMapper;
 
-  @Override
-  protected void configure(AuthenticationManagerBuilder auth) {
-    auth.authenticationProvider(authProvider);
-  }
-
   @Bean
-  @Override
-  public AuthenticationManager authenticationManagerBean() throws Exception {
-    return super.authenticationManagerBean();
+  public AuthenticationManager authenticationManager() {
+    return new ProviderManager(authProvider);
   }
 
   @Bean
   public MultipartResolver multipartResolver() {
-    return new CommonsMultipartResolver() {
-      @Override
-      public boolean isMultipart(HttpServletRequest request) {
-        String method = request.getMethod().toLowerCase();
-        // By default, only POST is allowed. Since this is an 'update' we 
should accept PUT.
-        if (!Arrays.asList("put", "post").contains(method)) {
-          return false;
-        }
-        String contentType = request.getContentType();
-        return (contentType != null && 
contentType.toLowerCase().startsWith("multipart/"));
-      }
-    };
+    // Spring 6.x uses StandardServletMultipartResolver instead of 
CommonsMultipartResolver
+    return new StandardServletMultipartResolver();
   }
 
-  protected void configure(HttpSecurity http) throws Exception {
-    
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
-        .authorizeRequests()
-        .antMatchers("/docs/**", "/swagger-ui.html", "/swagger-ui/index.html", 
"/swagger-ui/**",
-            "/", Links.URI_VERSION + "/api-docs/**", 
"/webjars/springdoc-openapi-ui/**",
-            "/v3/api-docs/**", "/swagger-resources/**")
-        .permitAll()
-        .and().csrf().disable();
+  @Bean
+  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+    http.sessionManagement(
+        session -> 
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+        .authorizeHttpRequests(authorize -> authorize
+            .requestMatchers(new AntPathRequestMatcher("/docs/**"),
+                new AntPathRequestMatcher("/swagger-ui.html"),
+                new AntPathRequestMatcher("/swagger-ui/index.html"),
+                new AntPathRequestMatcher("/swagger-ui/**"),
+                new AntPathRequestMatcher("/"),
+                new AntPathRequestMatcher("/v1/api-docs/**"),
+                new AntPathRequestMatcher("/webjars/springdoc-openapi-ui/**"),
+                new AntPathRequestMatcher("/v3/api-docs/**"),
+                new AntPathRequestMatcher("/swagger-resources/**"))
+            .permitAll())

Review Comment:
   Isn't this security configuration is incomplete? After defining permitAll() 
for documentation endpoints, there's no catch-all rule for other endpoints. 
   
   This means:
   
        1. All other endpoints are implicitly accessible without authentication
        2. Missing `.anyRequest().authenticated()` or similar authorization rule
        3. This creates a security bypass for the management REST API
   
   ```
      .authorizeHttpRequests(authorize -> authorize
          .requestMatchers(/* documentation paths */).permitAll()
          .anyRequest().authenticated()) <-- Should we add this line?
   ```



##########
geode-http-service/src/main/java/org/apache/geode/internal/cache/http/service/InternalHttpService.java:
##########
@@ -172,46 +254,88 @@ public synchronized void addWebApplication(String 
webAppContext, Path warFilePat
       Map<String, Object> attributeNameValuePairs)
       throws Exception {
     if (httpServer == null) {
-      logger.info(
-          String.format("unable to add %s webapp. Http service is not started 
on this member.",
-              webAppContext));
+      logger.warn(WEBAPP, "Cannot add webapp, HTTP service not started: 
webapp={}", webAppContext);
       return;
     }
 
+    logger.info(WEBAPP, "Adding webapp {}", webAppContext);
+
     WebAppContext webapp = new WebAppContext();
     webapp.setContextPath(webAppContext);
     webapp.setWar(warFilePath.toString());
+
+    // Required for Spring Boot initialization: AnnotationConfiguration 
triggers Jetty's annotation
+    // scanning during webapp.configure(), which discovers 
SpringServletContainerInitializer via
+    // ServiceLoader from META-INF/services. Without this, Spring's 
WebApplicationInitializer
+    // chain never starts, causing 404 errors for all REST endpoints.
+    // Reference: 
jetty-ee10-demos/embedded/src/main/java/ServerWithAnnotations.java
+    webapp.addConfiguration(new AnnotationConfiguration());
+
+    // Child-first classloading prevents parent classloader's Jackson from 
conflicting with
+    // webapp's bundled version, avoiding NoSuchMethodError during JSON 
serialization.
     webapp.setParentLoaderPriority(false);
 
     // GEODE-7334: load all jackson classes from war file except jackson 
annotations
-    
webapp.getSystemClasspathPattern().add("com.fasterxml.jackson.annotation.");
-    webapp.getServerClasspathPattern().add("com.fasterxml.jackson.",
-        "-com.fasterxml.jackson.annotation.");
+    // Jetty 12: Attribute names changed to ee10.webapp namespace
+    
webapp.setAttribute("org.eclipse.jetty.ee10.webapp.ContainerIncludeJarPattern",
+        ".*/jakarta\\.servlet-api-[^/]*\\.jar$|" +
+            ".*/jakarta\\.servlet\\.jsp\\.jstl-.*\\.jar$|" +
+            ".*/com\\.fasterxml\\.jackson\\.annotation\\..*\\.jar$");
+    
webapp.setAttribute("org.eclipse.jetty.ee10.webapp.WebInfIncludeJarPattern",
+        ".*/com\\.fasterxml\\.jackson\\.(?!annotation).*\\.jar$");
+
     // add the member's working dir as the extra classpath
     webapp.setExtraClasspath(new File(".").getAbsolutePath());
 
     webapp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", 
"false");
     webapp.addAliasCheck(new SymlinkAllowedResourceAliasChecker(webapp));
 
+    // Store attributes on WebAppContext for backward compatibility
     if (attributeNameValuePairs != null) {
       attributeNameValuePairs.forEach(webapp::setAttribute);
+
+      // Listener must be registered as Source.EMBEDDED to execute during 
ServletContext
+      // initialization, BEFORE DispatcherServlet starts. This timing 
guarantees Spring's
+      // dependency injection finds SecurityService when initializing 
LoginHandlerInterceptor.
+      // Using Source.JAVAX_API or adding via web.xml would execute too late 
in the lifecycle.
+      // Pattern reference: 
jetty-ee10/jetty-ee10-servlet/OneServletContext.java
+      ListenerHolder listenerHolder = new ListenerHolder(Source.EMBEDDED);
+      listenerHolder.setListener(
+          new ServletContextAttributeListener(attributeNameValuePairs, 
webAppContext));
+      webapp.getServletHandler().addListener(listenerHolder);
     }
 
     File tmpPath = new File(getWebAppBaseDirectory(webAppContext));
     tmpPath.mkdirs();
     webapp.setTempDirectory(tmpPath);
-    logger.info("Adding webapp " + webAppContext);
-    ((HandlerCollection) httpServer.getHandler()).addHandler(webapp);
-
-    // if the server is not started yet start the server, otherwise, start the 
webapp alone
-    if (!httpServer.isStarted()) {
-      logger.info("Attempting to start HTTP service on port ({}) at 
bind-address ({})...",
-          port, bindAddress);
-      httpServer.start();
-    } else {
-      webapp.start();
+
+    if (logger.isDebugEnabled()) {
+      ClassLoader webappClassLoader = webapp.getClassLoader();
+      ClassLoader parentClassLoader =
+          (webappClassLoader != null) ? webappClassLoader.getParent() : null;
+      logger.debug(CONFIG, "Webapp configuration: {}",
+          new LogContext()
+              .add("context", webAppContext)
+              .add("tempDir", tmpPath.getAbsolutePath())
+              .add("parentLoaderPriority", webapp.isParentLoaderPriority())
+              .add("webappClassLoader", webappClassLoader)
+              .add("parentClassLoader", parentClassLoader)
+              .add("annotationConfigEnabled", true)
+              .add("servletContextListenerAdded", attributeNameValuePairs != 
null));
     }
+
+    // In Jetty 12, Handler.Sequence replaced HandlerCollection for dynamic 
handler lists
+    ((Handler.Sequence) httpServer.getHandler()).addHandler(webapp);
+
+    // Server start deferred to restartHttpServer() to batch all webapp 
configurations,
+    // avoiding multiple restart cycles and ensuring all webapps initialize 
together.
     webApps.add(webapp);

Review Comment:
   Can this lead to a timing dependency where:
   
        1. Webapps are added to Handler.Sequence immediately (line 328)
        2. But server restart is deferred until restartHttpServer() is called
        3. If server is already running, webapps might be accessible before 
proper initialization
   
   
   Should we consider adding webapps to a staging list first, then batch-add 
them during restartHttpServer() to ensure atomic deployment?



##########
geode-http-service/src/main/java/org/apache/geode/internal/cache/http/service/InternalHttpService.java:
##########
@@ -64,18 +82,77 @@ public class InternalHttpService implements HttpService {
 
   private final List<WebAppContext> webApps = new ArrayList<>();
 
+  /**
+   * Bridges WebAppContext and ServletContext attribute namespaces in Jetty 12.
+   *
+   * <p>
+   * Why needed: In Jetty 12, WebAppContext.setAttribute() stores attributes 
in the webapp's
+   * context, but Spring's ServletContextAware beans (like 
LoginHandlerInterceptor) retrieve
+   * from ServletContext.getAttribute(). These are separate namespaces that 
don't auto-sync.
+   *
+   * <p>
+   * Timing: contextInitialized() is invoked BEFORE Spring's DispatcherServlet 
initializes,
+   * guaranteeing attributes are present when Spring beans request them during 
dependency injection.
+   * Without this, SecurityService would be null in LoginHandlerInterceptor, 
causing 503 errors.
+   */
+  private static class ServletContextAttributeListener implements 
ServletContextListener {

Review Comment:
   Having ServletContextAttributeListener is a clever solution to handle the 
namespace separation between WebAppContext and ServletContext in Jetty 12. This 
prevents Spring dependency injection failures. Excellent Solution



##########
build.gradle:
##########
@@ -58,7 +58,30 @@ allprojects {
 
   repositories {
     mavenCentral()
-    maven { url "https://repo.spring.io/release"; }
+    maven { 
+      url "https://jakarta.oss.sonatype.org/content/repositories/releases/";
+      name "Jakarta EE Releases"
+    }
+  }
+
+  // CRITICAL: Fix for Log4j/SLF4J circular binding conflict introduced during 
Jakarta/Spring Boot 3.x migration
+  // 
+  // Root Cause:
+  // - Geode uses Log4j Core directly and includes 'log4j-slf4j-impl' (routes 
SLF4J calls → Log4j)
+  // - Spring Boot 3.x includes 'spring-boot-starter-logging' which brings in 
'log4j-to-slf4j' (routes Log4j calls → SLF4J)
+  // - Having BOTH creates a circular binding: SLF4J → Log4j → SLF4J
+  // 
+  // Symptoms:
+  // - ClassCastException: org.apache.logging.slf4j.SLF4JLogger cannot be cast 
to org.apache.logging.log4j.core.Logger
+  // - ClassCastException: org.apache.logging.slf4j.SLF4JLoggerContext cannot 
be cast to org.apache.logging.log4j.core.LoggerContext
+  // - Occurs in Log4jLoggingProvider.getRootLoggerContext() when 
LogManager.getRootLogger() returns SLF4J logger instead of Log4j logger
+  //
+  // Solution:
+  // Exclude 'log4j-to-slf4j' globally. Geode's logging architecture requires 
Log4j Core to be the primary logging implementation,
+  // with SLF4J calls being routed TO Log4j (via log4j-slf4j-impl), not the 
other way around.
+  //
+  configurations.all {
+    exclude group: 'org.apache.logging.log4j', module: 'log4j-to-slf4j'

Review Comment:
   Excellent Fix for Logging Conflict. This global exclusion prevents 
ClassCastException issues that would occur with circular logging bindings



##########
geode-web-api/src/main/java/org/apache/geode/rest/internal/web/security/RestSecurityConfiguration.java:
##########
@@ -19,47 +19,159 @@
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.security.authentication.AuthenticationManager;
-import 
org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
-import 
org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.authentication.ProviderManager;
+import 
org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
 import 
org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import 
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import 
org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
 
-import org.apache.geode.management.configuration.Links;
 
 @Configuration
 @EnableWebSecurity
-@EnableGlobalMethodSecurity(prePostEnabled = true)
+// Spring Security 6.x migration: @EnableGlobalMethodSecurity deprecated, 
replaced by
+// @EnableMethodSecurity
+@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, 
jsr250Enabled = true)
 @ComponentScan("org.apache.geode.rest.internal.web")
-public class RestSecurityConfiguration extends WebSecurityConfigurerAdapter {
+public class RestSecurityConfiguration {
 
   @Autowired
   private GeodeAuthenticationProvider authProvider;
 
-  @Override
-  protected void configure(AuthenticationManagerBuilder auth) {
-    auth.authenticationProvider(authProvider);
-  }
-
+  /**
+   * Spring Security 6.x migration: Create AuthenticationManager bean using 
ProviderManager.
+   * Previously configured via AuthenticationManagerBuilder in configure() 
method.
+   */
   @Bean
-  @Override
-  public AuthenticationManager authenticationManagerBean() throws Exception {
-    return super.authenticationManagerBean();
+  public AuthenticationManager authenticationManager() {
+    return new ProviderManager(authProvider);
   }
 
-  @Override
-  protected void configure(HttpSecurity http) throws Exception {
+  /**
+   * Spring Security 6.x migration: Configure security using 
SecurityFilterChain bean.
+   * Replaces WebSecurityConfigurerAdapter's configure(HttpSecurity) method.
+   * Uses lambda-based configuration and authorizeHttpRequests() instead of 
authorizeRequests().
+   */
+  @Bean
+  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 
-    
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
-        .authorizeRequests()
-        .antMatchers("/docs/**", "/swagger-ui.html", "/swagger-ui/index.html", 
"/swagger-ui/**",
-            Links.URI_VERSION + "/api-docs/**", 
"/webjars/springdoc-openapi-ui/**",
-            "/v3/api-docs/**", "/swagger-resources/**")
-        .permitAll().and().csrf().disable();
+    http.sessionManagement(
+        session -> 
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+        .authorizeHttpRequests(authorize -> authorize
+            .requestMatchers(new AntPathRequestMatcher("/docs/**"),
+                new AntPathRequestMatcher("/swagger-ui.html"),
+                new AntPathRequestMatcher("/swagger-ui/index.html"),
+                new AntPathRequestMatcher("/swagger-ui/**"),
+                new AntPathRequestMatcher("/v1/api-docs/**"),
+                new AntPathRequestMatcher("/webjars/springdoc-openapi-ui/**"),
+                new AntPathRequestMatcher("/v3/api-docs/**"),
+                new AntPathRequestMatcher("/swagger-resources/**"))
+            .permitAll())

Review Comment:
   Isn't this security configuration is incomplete? After defining permitAll() 
for documentation endpoints, there's no catch-all rule for other endpoints. 
   
   This means:
   
        1. All other endpoints are implicitly accessible without authentication
        2. Missing `.anyRequest().authenticated()` or similar authorization rule
        3. This creates a security bypass for the management REST API
   
   ```
      .authorizeHttpRequests(authorize -> authorize
          .requestMatchers(/* documentation paths */).permitAll()
          .anyRequest().authenticated()) <-- Should we add this line?
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to