Skip to content

Commit 875af77

Browse files
lukaszlenartclaude
andcommitted
feat(proxy): WW-5514 add StrutsProxyService for proxy detection and resolution
Introduces a configurable ProxyService interface and StrutsProxyService implementation for detecting and resolving Spring AOP/Hibernate proxies. Key changes: - Add ProxyService interface with isProxy, ultimateTargetClass, and resolveTargetMember methods - Add StrutsProxyService implementation using configurable caches - Add ProxyCacheFactory and StrutsProxyCacheFactory for cache management - Integrate ProxyService into ChainingInterceptor, ParametersInterceptor, and SecurityMemberAccess - Add integration test with Spring AOP proxied action chaining - Add configuration constants for proxy cache type and size The StrutsProxyService correctly handles: - Spring CGLIB proxies (class-based) - Spring JDK dynamic proxies (interface-based) - Hibernate entity proxies - Member resolution for allowlist checking Fixes WW-5514 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 720e603 commit 875af77

29 files changed

Lines changed: 2044 additions & 79 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.struts2.showcase.proxy;
20+
21+
import org.aopalliance.intercept.MethodInterceptor;
22+
import org.aopalliance.intercept.MethodInvocation;
23+
import org.apache.logging.log4j.LogManager;
24+
import org.apache.logging.log4j.Logger;
25+
26+
/**
27+
* Simple AOP interceptor that wraps actions in a Spring proxy.
28+
* Used to test that Struts correctly handles Spring AOP proxied actions
29+
* in action chaining scenarios (WW-5514).
30+
*/
31+
public class LoggingInterceptor implements MethodInterceptor {
32+
33+
private static final Logger LOG = LogManager.getLogger(LoggingInterceptor.class);
34+
35+
@Override
36+
public Object invoke(MethodInvocation invocation) throws Throwable {
37+
LOG.debug("Invoking method: {} on target: {}",
38+
invocation.getMethod().getName(),
39+
invocation.getThis().getClass().getName());
40+
return invocation.proceed();
41+
}
42+
}

apps/showcase/src/main/resources/struts-actionchaining.xml

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,26 @@
2020
*/
2121
-->
2222
<!DOCTYPE struts PUBLIC
23-
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
24-
"https://struts.apache.org/dtds/struts-6.0.dtd">
23+
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
24+
"https://struts.apache.org/dtds/struts-6.0.dtd">
2525

2626
<struts>
27-
<package name="actionchaining" extends="struts-default" namespace="/actionchaining">
28-
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
29-
<result type="chain">actionChain2</result>
30-
</action>
31-
<action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2">
32-
<result type="chain">actionChain3</result>
33-
</action>
34-
<action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3">
35-
<result>/WEB-INF/actionchaining/actionChainingResult.jsp</result>
36-
</action>
37-
</package>
27+
<package name="actionchaining" extends="struts-default" namespace="/actionchaining">
28+
<action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
29+
<result type="chain">actionChain2</result>
30+
</action>
31+
<action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2">
32+
<result type="chain">actionChain3</result>
33+
</action>
34+
<action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3">
35+
<result>/WEB-INF/actionchaining/actionChainingResult.jsp</result>
36+
</action>
37+
38+
<!-- Spring AOP Proxied Action Chain Test (WW-5514) -->
39+
<action name="proxiedActionChain1" class="proxiedActionChain1">
40+
<result type="chain">actionChain2</result>
41+
</action>
42+
</package>
3843
</struts>
3944

4045

apps/showcase/src/main/resources/struts.xml

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,83 +20,88 @@
2020
*/
2121
-->
2222
<!DOCTYPE struts PUBLIC
23-
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
24-
"https://struts.apache.org/dtds/struts-6.0.dtd">
23+
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
24+
"https://struts.apache.org/dtds/struts-6.0.dtd">
2525

2626
<!-- START SNIPPET: xworkSample -->
2727
<struts>
2828

2929
<!-- Some or all of these can be flipped to true for debugging -->
30-
<constant name="struts.i18n.reload" value="false" />
31-
<constant name="struts.enable.DynamicMethodInvocation" value="true" />
32-
<constant name="struts.devMode" value="false" />
33-
<constant name="struts.configuration.xml.reload" value="false" />
34-
<constant name="struts.custom.i18n.resources" value="globalMessages" />
35-
<constant name="struts.action.extension" value="action,," />
36-
37-
<constant name="struts.allowlist.enable" value="true" />
38-
<constant name="struts.parameters.requireAnnotations" value="true" />
30+
<constant name="struts.i18n.reload" value="false"/>
31+
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>
32+
<constant name="struts.devMode" value="false"/>
33+
<constant name="struts.configuration.xml.reload" value="false"/>
34+
<constant name="struts.custom.i18n.resources" value="globalMessages"/>
35+
<constant name="struts.action.extension" value="action,,"/>
36+
37+
<constant name="struts.allowlist.enable" value="true"/>
38+
<constant name="struts.parameters.requireAnnotations" value="true"/>
3939
<constant name="struts.allowlist.packageNames" value="org.apache.struts2.showcase"/>
4040

41-
<constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase" />
42-
<constant name="struts.convention.result.path" value="/WEB-INF" />
41+
<!-- Enable Spring AOP proxy support for action chaining test (WW-5514) -->
42+
<constant name="struts.disallowProxyObjectAccess" value="false"/>
43+
44+
<constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase"/>
45+
<constant name="struts.convention.result.path" value="/WEB-INF"/>
4346

4447
<!-- Necessary for Showcase because default includes org.apache.struts2.* -->
45-
<constant name="struts.convention.exclude.packages" value="org.apache.struts.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*"/>
48+
<constant name="struts.convention.exclude.packages"
49+
value="org.apache.struts.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*"/>
4650

47-
<constant name="struts.freemarker.manager.classname" value="customFreemarkerManager" />
48-
<constant name="struts.serve.static" value="true" />
49-
<constant name="struts.serve.static.browserCache" value="false" />
51+
<constant name="struts.freemarker.manager.classname" value="customFreemarkerManager"/>
52+
<constant name="struts.serve.static" value="true"/>
53+
<constant name="struts.serve.static.browserCache" value="false"/>
5054

51-
<constant name="struts.action.excludePattern" value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js,/testServlet/.*"/>
55+
<constant name="struts.action.excludePattern"
56+
value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js,/testServlet/.*"/>
5257

53-
<include file="struts-interactive.xml" />
58+
<include file="struts-interactive.xml"/>
5459

55-
<include file="struts-hangman.xml" />
60+
<include file="struts-hangman.xml"/>
5661

5762
<include file="struts-tags.xml"/>
5863

59-
<include file="struts-validation.xml" />
64+
<include file="struts-validation.xml"/>
6065

61-
<include file="struts-actionchaining.xml" />
66+
<include file="struts-actionchaining.xml"/>
6267

63-
<include file="struts-fileupload.xml" />
68+
<include file="struts-fileupload.xml"/>
6469

65-
<include file="struts-person.xml" />
70+
<include file="struts-person.xml"/>
6671

67-
<include file="struts-wait.xml" />
72+
<include file="struts-wait.xml"/>
6873

69-
<include file="struts-token.xml" />
74+
<include file="struts-token.xml"/>
7075

71-
<include file="struts-model-driven.xml" />
76+
<include file="struts-model-driven.xml"/>
7277

73-
<include file="struts-filedownload.xml" />
78+
<include file="struts-filedownload.xml"/>
7479

75-
<include file="struts-conversion.xml" />
80+
<include file="struts-conversion.xml"/>
7681

77-
<include file="struts-freemarker.xml" />
82+
<include file="struts-freemarker.xml"/>
7883

79-
<include file="struts-tiles.xml" />
84+
<include file="struts-tiles.xml"/>
8085

81-
<include file="struts-xslt.xml" />
86+
<include file="struts-xslt.xml"/>
8287

83-
<include file="struts-async.xml" />
88+
<include file="struts-async.xml"/>
8489

85-
<include file="struts-dispatcher.xml" />
90+
<include file="struts-dispatcher.xml"/>
8691

87-
<include file="struts-params-annotation.xml" />
92+
<include file="struts-params-annotation.xml"/>
8893

8994
<package name="default" extends="struts-default">
9095
<interceptors>
9196
<interceptor-stack name="crudStack">
92-
<interceptor-ref name="checkbox" />
93-
<interceptor-ref name="params" />
94-
<interceptor-ref name="staticParams" />
95-
<interceptor-ref name="defaultStack" />
97+
<interceptor-ref name="checkbox"/>
98+
<interceptor-ref name="params"/>
99+
<interceptor-ref name="staticParams"/>
100+
<interceptor-ref name="defaultStack"/>
96101
</interceptor-stack>
97102
</interceptors>
98103

99-
<default-action-ref name="showcase" />
104+
<default-action-ref name="showcase"/>
100105

101106
<action name="showcase">
102107
<result>/WEB-INF/showcase.jsp</result>
@@ -125,7 +130,7 @@
125130
</action>
126131
<action name="edit" class="org.apache.struts2.showcase.action.SkillAction">
127132
<result>/WEB-INF/empmanager/editSkill.jsp</result>
128-
<interceptor-ref name="params" />
133+
<interceptor-ref name="params"/>
129134
<interceptor-ref name="basicStack"/>
130135
</action>
131136
<action name="save" class="org.apache.struts2.showcase.action.SkillAction" method="save">
@@ -146,9 +151,11 @@
146151
<interceptor-ref name="basicStack"/>
147152
</action>
148153
<action name="edit-*" class="org.apache.struts2.showcase.action.EmployeeAction">
149-
<param name="empId">{1}</param>
154+
<param name="empId">{1}</param>
150155
<result>/WEB-INF/empmanager/editEmployee.jsp</result>
151-
<interceptor-ref name="crudStack"><param name="validation.excludeMethods">execute</param></interceptor-ref>
156+
<interceptor-ref name="crudStack">
157+
<param name="validation.excludeMethods">execute</param>
158+
</interceptor-ref>
152159
</action>
153160
<action name="save" class="org.apache.struts2.showcase.action.EmployeeAction" method="save">
154161
<result name="input">/WEB-INF/empmanager/editEmployee.jsp</result>
@@ -168,5 +175,5 @@
168175

169176
</struts>
170177

171-
<!-- END SNIPPET: xworkSample -->
178+
<!-- END SNIPPET: xworkSample -->
172179

apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,25 @@
115115
<bean id="guessCharacterAction" class="org.apache.struts2.showcase.hangman.GuessCharacterAction" scope="prototype"/>
116116
<bean id="getUpdatedHangmanAction" class="org.apache.struts2.showcase.hangman.GetUpdatedHangmanAction"
117117
scope="prototype"/>
118+
119+
120+
<!-- Spring AOP Proxy Configuration for Action Chaining Test (WW-5514) -->
121+
<bean id="loggingInterceptor" class="org.apache.struts2.showcase.proxy.LoggingInterceptor"/>
122+
123+
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
124+
<property name="proxyTargetClass" value="true"/>
125+
<property name="beanNames">
126+
<list>
127+
<value>proxiedActionChain1</value>
128+
</list>
129+
</property>
130+
<property name="interceptorNames">
131+
<list>
132+
<value>loggingInterceptor</value>
133+
</list>
134+
</property>
135+
</bean>
136+
137+
<bean id="proxiedActionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1" scope="prototype"/>
118138
</beans>
119139

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package it.org.apache.struts2.showcase;
20+
21+
import org.htmlunit.WebClient;
22+
import org.htmlunit.html.HtmlPage;
23+
import org.junit.Test;
24+
25+
import static org.junit.Assert.assertTrue;
26+
27+
/**
28+
* Integration test verifying that Spring AOP proxied actions work correctly
29+
* with action chaining. This tests the WW-5514 StrutsProxyService integration.
30+
*
31+
* <p>The test uses a Spring AOP proxied version of ActionChain1 (proxiedActionChain1)
32+
* which is wrapped by {@link org.apache.struts2.showcase.proxy.LoggingInterceptor}.
33+
* The ChainingInterceptor must correctly resolve the target class through
34+
* StrutsProxyService to copy properties to the next action in the chain.</p>
35+
*/
36+
public class SpringProxyActionChainingTest {
37+
38+
/**
39+
* Tests that action chaining works correctly when the first action is a Spring AOP proxy.
40+
*
41+
* <p>This verifies that:
42+
* <ul>
43+
* <li>StrutsProxyService correctly identifies the Spring CGLIB proxy</li>
44+
* <li>ChainingInterceptor resolves the target class for property copying</li>
45+
* <li>Properties from the proxied ActionChain1 are correctly copied to ActionChain2</li>
46+
* </ul>
47+
* </p>
48+
*/
49+
@Test
50+
public void testProxiedActionChaining() throws Exception {
51+
try (final WebClient webClient = new WebClient()) {
52+
final HtmlPage page = webClient.getPage(
53+
ParameterUtils.getBaseUrl() + "/actionchaining/proxiedActionChain1!input"
54+
);
55+
56+
final String pageAsText = page.asNormalizedText();
57+
58+
// Verify properties were chained correctly despite proxy
59+
assertTrue("ActionChain1 property should be present",
60+
pageAsText.contains("Action Chain 1 Property 1: Property Set In Action Chain 1"));
61+
assertTrue("ActionChain2 property should be present",
62+
pageAsText.contains("Action Chain 2 Property 1: Property Set in Action Chain 2"));
63+
assertTrue("ActionChain3 property should be present",
64+
pageAsText.contains("Action Chain 3 Property 1: Property set in Action Chain 3"));
65+
}
66+
}
67+
}

core/src/main/java/org/apache/struts2/StrutsConstants.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,35 @@ public final class StrutsConstants {
516516
*/
517517
public static final String STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE = "struts.ognl.expressionCacheMaxSize";
518518

519+
/**
520+
* Specifies the type of cache to use for proxy detection. Valid values defined in
521+
* {@link org.apache.struts2.ognl.OgnlCacheFactory.CacheType}.
522+
*
523+
* @since 7.2.0
524+
*/
525+
public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType";
526+
527+
/**
528+
* Specifies the maximum cache size for proxy detection caches.
529+
*
530+
* @since 7.2.0
531+
*/
532+
public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize";
533+
534+
/**
535+
* The {@link org.apache.struts2.ognl.ProxyCacheFactory} implementation class.
536+
*
537+
* @since 7.2.0
538+
*/
539+
public static final String STRUTS_PROXY_CACHE_FACTORY = "struts.proxy.cacheFactory";
540+
541+
/**
542+
* The {@link org.apache.struts2.util.ProxyService} implementation class.
543+
*
544+
* @since 7.2.0
545+
*/
546+
public static final String STRUTS_PROXYSERVICE = "struts.proxyService";
547+
519548
/**
520549
* Enables evaluation of OGNL expressions
521550
*

core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.apache.struts2.ognl.BeanInfoCacheFactory;
6262
import org.apache.struts2.ognl.ExpressionCacheFactory;
6363
import org.apache.struts2.ognl.OgnlGuard;
64+
import org.apache.struts2.ognl.ProxyCacheFactory;
6465
import org.apache.struts2.ognl.SecurityMemberAccess;
6566
import org.apache.struts2.ognl.accessor.RootAccessor;
6667
import org.apache.struts2.security.AcceptedPatternsChecker;
@@ -72,6 +73,7 @@
7273
import org.apache.struts2.url.UrlEncoder;
7374
import org.apache.struts2.util.ContentTypeMatcher;
7475
import org.apache.struts2.util.PatternMatcher;
76+
import org.apache.struts2.util.ProxyService;
7577
import org.apache.struts2.util.TextParser;
7678
import org.apache.struts2.util.ValueStackFactory;
7779
import org.apache.struts2.util.location.LocatableProperties;
@@ -442,6 +444,8 @@ public void register(ContainerBuilder builder, LocatableProperties props) {
442444

443445
alias(ExpressionCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_FACTORY, builder, props, Scope.SINGLETON);
444446
alias(BeanInfoCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_FACTORY, builder, props, Scope.SINGLETON);
447+
alias(ProxyCacheFactory.class, StrutsConstants.STRUTS_PROXY_CACHE_FACTORY, builder, props, Scope.SINGLETON);
448+
alias(ProxyService.class, StrutsConstants.STRUTS_PROXYSERVICE, builder, props, Scope.SINGLETON);
445449

446450
alias(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE);
447451
alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON);

0 commit comments

Comments
 (0)