Run boot war on standalone Apache Tomcat#3825
Conversation
There was a problem hiding this comment.
Pull request overview
Enables UAA to run as a classic servlet WAR on an externally managed Apache Tomcat by correcting filter bean naming/registration, and adds CI + local tooling to run integration tests against a standalone Tomcat distribution aligned with the Spring Boot BOM.
Changes:
- Split zone path/session filters into distinct
Filterbeans (forDelegatingFilterProxyin external Tomcat WAR) and separately namedFilterRegistrationBeans (for Spring Boot registration). - Add standalone Tomcat integration-test scripts and a new GitHub Actions job to run them.
- Improve test task logging defaults and add helper Gradle tasks to resolve/write the Tomcat distribution version.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/TokenExchangeGrantEndpointDocs.java | Update qualifiers to reference the new registration-bean names. |
| uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/token/JwtBearerGrantEndpointDocs.java | Update qualifiers to reference the new registration-bean names. |
| uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/EndpointDocs.java | Update qualifiers to reference the new registration-bean names. |
| uaa/src/test/java/org/cloudfoundry/identity/uaa/login/TokenEndpointDocs.java | Update qualifiers to reference the new registration-bean names. |
| uaa/src/test/java/org/cloudfoundry/identity/uaa/DefaultTestContext.java | Update MockMvc wiring to inject the registration beans using the new qualifiers. |
| uaa/build.gradle | Allow integration tests to run against an externally managed server (skip embedded java -jar lifecycle). |
| server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZonePathContextRewritingFilterConfiguration.java | Provide both Filter and FilterRegistrationBean beans with separate names to support external Tomcat WAR bootstrap. |
| server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZonePathContextRewritingFilter.java | Add a constant for the registration-bean name. |
| server/src/main/java/org/cloudfoundry/identity/uaa/zone/ZoneContextPathSessionFilter.java | Add a constant for the registration-bean name. |
| scripts/tomcat/kill_tomcat.sh | Add helper to stop Tomcat and kill processes bound to the port. |
| scripts/tomcat/integration_tests_tomcat.sh | Add end-to-end standalone Tomcat download/deploy/run script for integration tests. |
| scripts/integration_tests.sh | Document verbose per-test logging toggle. |
| build.gradle | Default per-test logging to include passed/skipped/failed; add tasks to write/print the Tomcat distribution version. |
| .github/workflows/integration-tests.yml | Add a new standalone Tomcat integration-test job and upload artifacts on failure. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @Bean(ZonePathContextRewritingFilter.BEAN_NAME) | ||
| ZonePathContextRewritingFilter zonePathContextRewritingFilter( | ||
| @Value("${zones.paths.enabled:false}") boolean zonePathsEnabled) { | ||
| return new ZonePathContextRewritingFilter(zonePathsEnabled); | ||
| } | ||
|
|
||
| /** | ||
| * Zone path rewriting runs as a servlet filter (enabled) so it executes before Spring Security | ||
| * selects a filter chain. That way the request path is rewritten to context path + servlet path | ||
| * (e.g. /z/myzone + /Codes) before security matchers run, so patterns like /Codes/** match correctly. | ||
| */ | ||
| @Bean(ZonePathContextRewritingFilter.BEAN_NAME) | ||
| FilterRegistrationBean<ZonePathContextRewritingFilter> zonePathContextRewritingFilter( | ||
| @Value("${zones.paths.enabled:false}") boolean zonePathsEnabled) { | ||
| ZonePathContextRewritingFilter filter = new ZonePathContextRewritingFilter(zonePathsEnabled); | ||
| @Bean(ZonePathContextRewritingFilter.REGISTRATION_BEAN_NAME) | ||
| FilterRegistrationBean<ZonePathContextRewritingFilter> zonePathContextRewritingFilterRegistration( | ||
| @Qualifier(ZonePathContextRewritingFilter.BEAN_NAME) ZonePathContextRewritingFilter filter) { | ||
| FilterRegistrationBean<ZonePathContextRewritingFilter> bean = new FilterRegistrationBean<>(filter); |
There was a problem hiding this comment.
ZonePathContextRewritingFilter is now defined as a Filter bean and also registered via a FilterRegistrationBean. In a Spring Boot embedded container, Filter-typed beans are eligible for automatic servlet filter registration, which can result in this filter being registered twice (once implicitly, once via the explicit registration bean). For this specific filter, a double invocation can corrupt ZONE_ORIGINAL_CONTEXT_PATH and cookie path rewriting (the second pass sees a different contextPath). Consider restructuring so only one servlet filter registration exists in Boot mode (e.g., rely solely on FilterRegistrationBean by not exposing a Filter-typed bean, or drop the FilterRegistrationBean and use @Order on the filter bean).
| @Bean(ZoneContextPathSessionFilter.BEAN_NAME) | ||
| ZoneContextPathSessionFilter zoneContextPathSessionFilter(TimeService timeService) { | ||
| return new ZoneContextPathSessionFilter(timeService); | ||
| } | ||
|
|
||
| /** | ||
| * Registers ZoneContextPathSessionFilter to run AFTER SessionRepositoryFilter (HIGHEST_PRECEDENCE + 50) | ||
| * so that it wraps the Spring Session-backed request. This ensures request.getSession() first goes | ||
| * through ZoneContextPathSessionFilter (returning a zone-scoped subsession view) which delegates | ||
| * to SessionRepositoryFilter's session (the actual Spring Session-backed session). | ||
| */ | ||
| @Bean(ZoneContextPathSessionFilter.BEAN_NAME) | ||
| FilterRegistrationBean<ZoneContextPathSessionFilter> zoneContextPathSessionFilter(TimeService timeService) { | ||
| ZoneContextPathSessionFilter filter = new ZoneContextPathSessionFilter(timeService); | ||
| @Bean(ZoneContextPathSessionFilter.REGISTRATION_BEAN_NAME) | ||
| FilterRegistrationBean<ZoneContextPathSessionFilter> zoneContextPathSessionFilterRegistration( | ||
| @Qualifier(ZoneContextPathSessionFilter.BEAN_NAME) ZoneContextPathSessionFilter filter) { | ||
| FilterRegistrationBean<ZoneContextPathSessionFilter> bean = new FilterRegistrationBean<>(filter); |
There was a problem hiding this comment.
Same concern as the zone path rewriting filter: ZoneContextPathSessionFilter is now exposed as a Filter bean and also registered via a FilterRegistrationBean. If Spring Boot auto-registers Filter beans, this can lead to two registrations with different filter names/orders, and the session-wrapping logic could be applied twice or in the wrong position relative to SessionRepositoryFilter. Recommend ensuring there is exactly one servlet registration for this filter in embedded Boot mode.
strehle
left a comment
There was a problem hiding this comment.
Please check the comments from copilot
|
Double checked filter registration, no duplicate calls are possible. |
#3819
Summary
Tomcat filter / WAR startup
8bac4bbdso zone path filters work on a classic servlet WAR on external Tomcat:Filterbeans use the existingBEAN_NAMEforDelegatingFilterProxy, andFilterRegistrationBeanbeans use newREGISTRATION_BEAN_NAMEconstants. MockMvc/docs tests that inject the registration beans were updated to use the new qualifiers.Standalone Tomcat integration tests (CI + local)
writeTomcatDistributionVersion/printTomcatDistributionVersionresolve the Apache Tomcat distro version from the sametomcat-embed-coreversion as the Spring Boot BOM and writebuild/tomcat-distribution.version.cloudfoundry-identity-uaaintegrationTest: IfUAA_INTEGRATION_SERVER=externalor-PuaaIntegrationServer=external, skips the embeddedjava -jarstart/stop (for use when Tomcat is already running). UsesrootProject.findPropertyso-Pworks from the root build.scripts/tomcat/:integration_tests_tomcat.sh— downloads/extracts Tomcat, writessetenv.sh(JVM flags aligned with boot integration tests; noset -uso Tomcat’ssetclasspath.shworks), deploysuaa.war, addsconf/Catalina/localhost/uaa.xmlwithuseRelativeRedirects="false"so redirectLocationheaders are absolute (fixes HttpClient 5 “Target host is not specified” after form login), waits on HTTP readiness, runsintegrationTestwith external server, stops Tomcat. Includes early port cleanup and stale pid removal so a failed run doesn’t leave 8080 occupied or confuse the next run.kill_tomcat.sh—shutdown.shwhenCATALINA_HOMEis set, thenlsof+ port kill.tomcat-integration-testoncfidentity/uaa-postgresql-15, runningscripts/tomcat/integration_tests_tomcat.shwithpostgresql,default.Test logging
test/integrationTesttasks: per-test PASSED/SKIPPED/FAILED logging is on by default; disable withUAA_VERBOSE_TESTS/-PverboseTestsinfalse/0/off/no. Documented on the integration scripts.Layout / hygiene
scripts/tomcat/(not the repo rootscripts/); CI and paths were updated accordingly.