This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch test/WW-5514-spring-proxy-integration-test in repository https://gitbox.apache.org/repos/asf/struts.git
commit 51db60f5b993100118a6b8cc958b63422e50ca88 Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Feb 9 10:57:22 2026 +0200 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 <[email protected]> --- .../struts2/showcase/proxy/LoggingInterceptor.java | 23 +- .../src/main/resources/struts-actionchaining.xml | 31 +- apps/showcase/src/main/resources/struts.xml | 95 +++--- .../src/main/webapp/WEB-INF/applicationContext.xml | 20 ++ .../showcase/SpringProxyActionChainingTest.java | 67 ++++ .../java/org/apache/struts2/StrutsConstants.java | 29 ++ .../config/StrutsBeanSelectionProvider.java | 4 + .../struts2/config/impl/DefaultConfiguration.java | 8 + .../struts2/interceptor/ChainingInterceptor.java | 17 +- .../parameter/ParametersInterceptor.java | 12 +- .../org/apache/struts2/ognl/ProxyCacheFactory.java | 27 ++ .../apache/struts2/ognl/SecurityMemberAccess.java | 19 +- .../struts2/ognl/StrutsProxyCacheFactory.java | 39 +++ .../java/org/apache/struts2/util/ProxyService.java | 101 ++++++ .../java/org/apache/struts2/util/ProxyUtil.java | 40 ++- .../{ProxyUtil.java => StrutsProxyService.java} | 166 ++++----- .../org/apache/struts2/default.properties | 13 + core/src/main/resources/struts-beans.xml | 4 + .../parameter/StrutsParameterAnnotationTest.java | 5 + .../struts2/ognl/SecurityMemberAccessTest.java | 5 + .../struts2/ognl/StrutsProxyCacheFactoryTest.java | 85 +++++ .../struts2/util/StrutsProxyServiceTest.java | 120 +++++++ .../org/test/ExternalSecurityMemberAccessTest.java | 1 + .../org/apache/struts2/json/DefaultJSONWriter.java | 10 +- .../org/apache/struts2/json/JSONResultTest.java | 8 +- .../ognl/SecurityMemberAccessProxyTest.java | 9 +- ...2026-02-07-WW-5514-proxy-cache-configuration.md | 372 +++++++++++++++++++++ .../validation/2026-02-08-WW-5514-validation.md | 175 ++++++++++ 28 files changed, 1319 insertions(+), 186 deletions(-) diff --git a/core/src/test/java/org/test/ExternalSecurityMemberAccessTest.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/proxy/LoggingInterceptor.java similarity index 50% copy from core/src/test/java/org/test/ExternalSecurityMemberAccessTest.java copy to apps/showcase/src/main/java/org/apache/struts2/showcase/proxy/LoggingInterceptor.java index 430979057..4a5dd40a7 100644 --- a/core/src/test/java/org/test/ExternalSecurityMemberAccessTest.java +++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/proxy/LoggingInterceptor.java @@ -16,18 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -package org.test; +package org.apache.struts2.showcase.proxy; -import org.apache.struts2.ognl.SecurityMemberAccessTest; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** - * Runs the same test suite using a SecurityMemberAccess class that is outside the - * org.apache.struts2.ognl package. + * Simple AOP interceptor that wraps actions in a Spring proxy. + * Used to test that Struts correctly handles Spring AOP proxied actions + * in action chaining scenarios (WW-5514). */ -public class ExternalSecurityMemberAccessTest extends SecurityMemberAccessTest { +public class LoggingInterceptor implements MethodInterceptor { + + private static final Logger LOG = LogManager.getLogger(LoggingInterceptor.class); @Override - protected void assignNewSmaHelper() { - sma = new ExternalSecurityMemberAccess(mockedProviderAllowlist, mockedThreadAllowlist); + public Object invoke(MethodInvocation invocation) throws Throwable { + LOG.debug("Invoking method: {} on target: {}", + invocation.getMethod().getName(), + invocation.getThis().getClass().getName()); + return invocation.proceed(); } } diff --git a/apps/showcase/src/main/resources/struts-actionchaining.xml b/apps/showcase/src/main/resources/struts-actionchaining.xml index 4f39940f0..ae2a7461c 100644 --- a/apps/showcase/src/main/resources/struts-actionchaining.xml +++ b/apps/showcase/src/main/resources/struts-actionchaining.xml @@ -20,21 +20,26 @@ */ --> <!DOCTYPE struts PUBLIC - "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN" - "https://struts.apache.org/dtds/struts-6.0.dtd"> + "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN" + "https://struts.apache.org/dtds/struts-6.0.dtd"> <struts> - <package name="actionchaining" extends="struts-default" namespace="/actionchaining"> - <action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1"> - <result type="chain">actionChain2</result> - </action> - <action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2"> - <result type="chain">actionChain3</result> - </action> - <action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3"> - <result>/WEB-INF/actionchaining/actionChainingResult.jsp</result> - </action> - </package> + <package name="actionchaining" extends="struts-default" namespace="/actionchaining"> + <action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1"> + <result type="chain">actionChain2</result> + </action> + <action name="actionChain2" class="org.apache.struts2.showcase.actionchaining.ActionChain2"> + <result type="chain">actionChain3</result> + </action> + <action name="actionChain3" class="org.apache.struts2.showcase.actionchaining.ActionChain3"> + <result>/WEB-INF/actionchaining/actionChainingResult.jsp</result> + </action> + + <!-- Spring AOP Proxied Action Chain Test (WW-5514) --> + <action name="proxiedActionChain1" class="proxiedActionChain1"> + <result type="chain">actionChain2</result> + </action> + </package> </struts> diff --git a/apps/showcase/src/main/resources/struts.xml b/apps/showcase/src/main/resources/struts.xml index 5c1cf37ff..5dbe07cee 100644 --- a/apps/showcase/src/main/resources/struts.xml +++ b/apps/showcase/src/main/resources/struts.xml @@ -20,83 +20,88 @@ */ --> <!DOCTYPE struts PUBLIC - "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN" - "https://struts.apache.org/dtds/struts-6.0.dtd"> + "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN" + "https://struts.apache.org/dtds/struts-6.0.dtd"> <!-- START SNIPPET: xworkSample --> <struts> <!-- Some or all of these can be flipped to true for debugging --> - <constant name="struts.i18n.reload" value="false" /> - <constant name="struts.enable.DynamicMethodInvocation" value="true" /> - <constant name="struts.devMode" value="false" /> - <constant name="struts.configuration.xml.reload" value="false" /> - <constant name="struts.custom.i18n.resources" value="globalMessages" /> - <constant name="struts.action.extension" value="action,," /> - - <constant name="struts.allowlist.enable" value="true" /> - <constant name="struts.parameters.requireAnnotations" value="true" /> + <constant name="struts.i18n.reload" value="false"/> + <constant name="struts.enable.DynamicMethodInvocation" value="true"/> + <constant name="struts.devMode" value="false"/> + <constant name="struts.configuration.xml.reload" value="false"/> + <constant name="struts.custom.i18n.resources" value="globalMessages"/> + <constant name="struts.action.extension" value="action,,"/> + + <constant name="struts.allowlist.enable" value="true"/> + <constant name="struts.parameters.requireAnnotations" value="true"/> <constant name="struts.allowlist.packageNames" value="org.apache.struts2.showcase"/> - <constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase" /> - <constant name="struts.convention.result.path" value="/WEB-INF" /> + <!-- Enable Spring AOP proxy support for action chaining test (WW-5514) --> + <constant name="struts.disallowProxyObjectAccess" value="false"/> + + <constant name="struts.convention.package.locators.basePackage" value="org.apache.struts2.showcase"/> + <constant name="struts.convention.result.path" value="/WEB-INF"/> <!-- Necessary for Showcase because default includes org.apache.struts2.* --> - <constant name="struts.convention.exclude.packages" value="org.apache.struts.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*"/> + <constant name="struts.convention.exclude.packages" + value="org.apache.struts.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*"/> - <constant name="struts.freemarker.manager.classname" value="customFreemarkerManager" /> - <constant name="struts.serve.static" value="true" /> - <constant name="struts.serve.static.browserCache" value="false" /> + <constant name="struts.freemarker.manager.classname" value="customFreemarkerManager"/> + <constant name="struts.serve.static" value="true"/> + <constant name="struts.serve.static.browserCache" value="false"/> - <constant name="struts.action.excludePattern" value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js,/testServlet/.*"/> + <constant name="struts.action.excludePattern" + value=".*/images/.*\.gif,.*/img/.*\.gif,.*/styles/.*\.css,.*/js/.*\.js,/testServlet/.*"/> - <include file="struts-interactive.xml" /> + <include file="struts-interactive.xml"/> - <include file="struts-hangman.xml" /> + <include file="struts-hangman.xml"/> <include file="struts-tags.xml"/> - <include file="struts-validation.xml" /> + <include file="struts-validation.xml"/> - <include file="struts-actionchaining.xml" /> + <include file="struts-actionchaining.xml"/> - <include file="struts-fileupload.xml" /> + <include file="struts-fileupload.xml"/> - <include file="struts-person.xml" /> + <include file="struts-person.xml"/> - <include file="struts-wait.xml" /> + <include file="struts-wait.xml"/> - <include file="struts-token.xml" /> + <include file="struts-token.xml"/> - <include file="struts-model-driven.xml" /> + <include file="struts-model-driven.xml"/> - <include file="struts-filedownload.xml" /> + <include file="struts-filedownload.xml"/> - <include file="struts-conversion.xml" /> + <include file="struts-conversion.xml"/> - <include file="struts-freemarker.xml" /> + <include file="struts-freemarker.xml"/> - <include file="struts-tiles.xml" /> + <include file="struts-tiles.xml"/> - <include file="struts-xslt.xml" /> + <include file="struts-xslt.xml"/> - <include file="struts-async.xml" /> + <include file="struts-async.xml"/> - <include file="struts-dispatcher.xml" /> + <include file="struts-dispatcher.xml"/> - <include file="struts-params-annotation.xml" /> + <include file="struts-params-annotation.xml"/> <package name="default" extends="struts-default"> <interceptors> <interceptor-stack name="crudStack"> - <interceptor-ref name="checkbox" /> - <interceptor-ref name="params" /> - <interceptor-ref name="staticParams" /> - <interceptor-ref name="defaultStack" /> + <interceptor-ref name="checkbox"/> + <interceptor-ref name="params"/> + <interceptor-ref name="staticParams"/> + <interceptor-ref name="defaultStack"/> </interceptor-stack> </interceptors> - <default-action-ref name="showcase" /> + <default-action-ref name="showcase"/> <action name="showcase"> <result>/WEB-INF/showcase.jsp</result> @@ -125,7 +130,7 @@ </action> <action name="edit" class="org.apache.struts2.showcase.action.SkillAction"> <result>/WEB-INF/empmanager/editSkill.jsp</result> - <interceptor-ref name="params" /> + <interceptor-ref name="params"/> <interceptor-ref name="basicStack"/> </action> <action name="save" class="org.apache.struts2.showcase.action.SkillAction" method="save"> @@ -146,9 +151,11 @@ <interceptor-ref name="basicStack"/> </action> <action name="edit-*" class="org.apache.struts2.showcase.action.EmployeeAction"> - <param name="empId">{1}</param> + <param name="empId">{1}</param> <result>/WEB-INF/empmanager/editEmployee.jsp</result> - <interceptor-ref name="crudStack"><param name="validation.excludeMethods">execute</param></interceptor-ref> + <interceptor-ref name="crudStack"> + <param name="validation.excludeMethods">execute</param> + </interceptor-ref> </action> <action name="save" class="org.apache.struts2.showcase.action.EmployeeAction" method="save"> <result name="input">/WEB-INF/empmanager/editEmployee.jsp</result> @@ -168,5 +175,5 @@ </struts> -<!-- END SNIPPET: xworkSample --> + <!-- END SNIPPET: xworkSample --> diff --git a/apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml b/apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml index ef700ef48..788890326 100644 --- a/apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml +++ b/apps/showcase/src/main/webapp/WEB-INF/applicationContext.xml @@ -115,5 +115,25 @@ <bean id="guessCharacterAction" class="org.apache.struts2.showcase.hangman.GuessCharacterAction" scope="prototype"/> <bean id="getUpdatedHangmanAction" class="org.apache.struts2.showcase.hangman.GetUpdatedHangmanAction" scope="prototype"/> + + + <!-- Spring AOP Proxy Configuration for Action Chaining Test (WW-5514) --> + <bean id="loggingInterceptor" class="org.apache.struts2.showcase.proxy.LoggingInterceptor"/> + + <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> + <property name="proxyTargetClass" value="true"/> + <property name="beanNames"> + <list> + <value>proxiedActionChain1</value> + </list> + </property> + <property name="interceptorNames"> + <list> + <value>loggingInterceptor</value> + </list> + </property> + </bean> + + <bean id="proxiedActionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1" scope="prototype"/> </beans> diff --git a/apps/showcase/src/test/java/it/org/apache/struts2/showcase/SpringProxyActionChainingTest.java b/apps/showcase/src/test/java/it/org/apache/struts2/showcase/SpringProxyActionChainingTest.java new file mode 100644 index 000000000..8b3a9794c --- /dev/null +++ b/apps/showcase/src/test/java/it/org/apache/struts2/showcase/SpringProxyActionChainingTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package it.org.apache.struts2.showcase; + +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlPage; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +/** + * Integration test verifying that Spring AOP proxied actions work correctly + * with action chaining. This tests the WW-5514 StrutsProxyService integration. + * + * <p>The test uses a Spring AOP proxied version of ActionChain1 (proxiedActionChain1) + * which is wrapped by {@link org.apache.struts2.showcase.proxy.LoggingInterceptor}. + * The ChainingInterceptor must correctly resolve the target class through + * StrutsProxyService to copy properties to the next action in the chain.</p> + */ +public class SpringProxyActionChainingTest { + + /** + * Tests that action chaining works correctly when the first action is a Spring AOP proxy. + * + * <p>This verifies that: + * <ul> + * <li>StrutsProxyService correctly identifies the Spring CGLIB proxy</li> + * <li>ChainingInterceptor resolves the target class for property copying</li> + * <li>Properties from the proxied ActionChain1 are correctly copied to ActionChain2</li> + * </ul> + * </p> + */ + @Test + public void testProxiedActionChaining() throws Exception { + try (final WebClient webClient = new WebClient()) { + final HtmlPage page = webClient.getPage( + ParameterUtils.getBaseUrl() + "/actionchaining/proxiedActionChain1!input" + ); + + final String pageAsText = page.asNormalizedText(); + + // Verify properties were chained correctly despite proxy + assertTrue("ActionChain1 property should be present", + pageAsText.contains("Action Chain 1 Property 1: Property Set In Action Chain 1")); + assertTrue("ActionChain2 property should be present", + pageAsText.contains("Action Chain 2 Property 1: Property Set in Action Chain 2")); + assertTrue("ActionChain3 property should be present", + pageAsText.contains("Action Chain 3 Property 1: Property set in Action Chain 3")); + } + } +} diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index a66478a97..418c55e44 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -516,6 +516,35 @@ public final class StrutsConstants { */ public static final String STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE = "struts.ognl.expressionCacheMaxSize"; + /** + * Specifies the type of cache to use for proxy detection. Valid values defined in + * {@link org.apache.struts2.ognl.OgnlCacheFactory.CacheType}. + * + * @since 7.2.0 + */ + public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType"; + + /** + * Specifies the maximum cache size for proxy detection caches. + * + * @since 7.2.0 + */ + public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize"; + + /** + * The {@link org.apache.struts2.ognl.ProxyCacheFactory} implementation class. + * + * @since 7.2.0 + */ + public static final String STRUTS_PROXY_CACHE_FACTORY = "struts.proxy.cacheFactory"; + + /** + * The {@link org.apache.struts2.util.ProxyService} implementation class. + * + * @since 7.2.0 + */ + public static final String STRUTS_PROXYSERVICE = "struts.proxyService"; + /** * Enables evaluation of OGNL expressions * diff --git a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java index eda65e527..c584a4f58 100644 --- a/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java +++ b/core/src/main/java/org/apache/struts2/config/StrutsBeanSelectionProvider.java @@ -61,6 +61,7 @@ import org.apache.struts2.interceptor.exec.ExecutorProvider; import org.apache.struts2.ognl.BeanInfoCacheFactory; import org.apache.struts2.ognl.ExpressionCacheFactory; import org.apache.struts2.ognl.OgnlGuard; +import org.apache.struts2.ognl.ProxyCacheFactory; import org.apache.struts2.ognl.SecurityMemberAccess; import org.apache.struts2.ognl.accessor.RootAccessor; import org.apache.struts2.security.AcceptedPatternsChecker; @@ -72,6 +73,7 @@ import org.apache.struts2.url.UrlDecoder; import org.apache.struts2.url.UrlEncoder; import org.apache.struts2.util.ContentTypeMatcher; import org.apache.struts2.util.PatternMatcher; +import org.apache.struts2.util.ProxyService; import org.apache.struts2.util.TextParser; import org.apache.struts2.util.ValueStackFactory; import org.apache.struts2.util.location.LocatableProperties; @@ -442,6 +444,8 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider { alias(ExpressionCacheFactory.class, StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_FACTORY, builder, props, Scope.SINGLETON); alias(BeanInfoCacheFactory.class, StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_FACTORY, builder, props, Scope.SINGLETON); + alias(ProxyCacheFactory.class, StrutsConstants.STRUTS_PROXY_CACHE_FACTORY, builder, props, Scope.SINGLETON); + alias(ProxyService.class, StrutsConstants.STRUTS_PROXYSERVICE, builder, props, Scope.SINGLETON); alias(SecurityMemberAccess.class, StrutsConstants.STRUTS_MEMBER_ACCESS, builder, props, Scope.PROTOTYPE); alias(OgnlGuard.class, StrutsConstants.STRUTS_OGNL_GUARD, builder, props, Scope.SINGLETON); diff --git a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java index 48791c919..9ec9f9c20 100644 --- a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java +++ b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java @@ -85,13 +85,17 @@ import org.apache.struts2.ognl.ExpressionCacheFactory; import org.apache.struts2.ognl.OgnlCacheFactory; import org.apache.struts2.ognl.OgnlReflectionProvider; import org.apache.struts2.ognl.OgnlUtil; +import org.apache.struts2.ognl.ProxyCacheFactory; +import org.apache.struts2.ognl.StrutsProxyCacheFactory; import org.apache.struts2.ognl.OgnlValueStackFactory; import org.apache.struts2.ognl.SecurityMemberAccess; import org.apache.struts2.ognl.accessor.CompoundRootAccessor; import org.apache.struts2.ognl.accessor.RootAccessor; import org.apache.struts2.ognl.accessor.XWorkMethodAccessor; +import org.apache.struts2.util.StrutsProxyService; import org.apache.struts2.util.OgnlTextParser; import org.apache.struts2.util.PatternMatcher; +import org.apache.struts2.util.ProxyService; import org.apache.struts2.text.StrutsLocalizedTextProvider; import org.apache.struts2.util.TextParser; import org.apache.struts2.util.ValueStack; @@ -144,6 +148,8 @@ public class DefaultConfiguration implements Configuration { constants.put(StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, 10000); constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC); constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, 10000); + constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC); + constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000); constants.put(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE); BOOTSTRAP_CONSTANTS = Collections.unmodifiableMap(constants); } @@ -395,6 +401,8 @@ public class DefaultConfiguration implements Configuration { .factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON) .factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON) + .factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON) + .factory(ProxyService.class, StrutsProxyService.class, Scope.SINGLETON) .factory(OgnlUtil.class, Scope.SINGLETON) .factory(SecurityMemberAccess.class, Scope.PROTOTYPE) .factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON) diff --git a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java index 2837d4c10..9c18d8869 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java @@ -27,7 +27,7 @@ import org.apache.struts2.inject.Inject; import org.apache.struts2.result.ActionChainResult; import org.apache.struts2.result.Result; import org.apache.struts2.util.CompoundRoot; -import org.apache.struts2.util.ProxyUtil; +import org.apache.struts2.util.ProxyService; import org.apache.struts2.util.TextParseUtil; import org.apache.struts2.util.ValueStack; import org.apache.struts2.util.reflection.ReflectionProvider; @@ -96,7 +96,7 @@ import java.util.Map; * </p> * <!-- END SNIPPET: extending --> * <u>Example code:</u> - * + * <p> * <!-- START SNIPPET: example --> * <pre> * <action name="someAction" class="com.examples.SomeAction"> @@ -114,7 +114,6 @@ import java.util.Map; * </pre> * <!-- END SNIPPET: example --> * - * * @author mrdon * @author tm_jee ( tm_jee(at)yahoo.co.uk ) * @see ActionChainResult @@ -135,12 +134,18 @@ public class ChainingInterceptor extends AbstractInterceptor { protected Collection<String> includes; protected ReflectionProvider reflectionProvider; + private ProxyService proxyService; @Inject public void setReflectionProvider(ReflectionProvider prov) { this.reflectionProvider = prov; } + @Inject + public void setProxyService(ProxyService proxyService) { + this.proxyService = proxyService; + } + @Inject(value = StrutsConstants.STRUTS_CHAINING_COPY_ERRORS, required = false) public void setCopyErrors(String copyErrors) { this.copyErrors = "true".equalsIgnoreCase(copyErrors); @@ -175,8 +180,8 @@ public class ChainingInterceptor extends AbstractInterceptor { } Object action = invocation.getAction(); Class<?> editable = null; - if (ProxyUtil.isProxy(action)) { - editable = ProxyUtil.ultimateTargetClass(action); + if (proxyService.isProxy(action)) { + editable = proxyService.ultimateTargetClass(action); } reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable); } @@ -184,7 +189,7 @@ public class ChainingInterceptor extends AbstractInterceptor { private Collection<String> prepareExcludes() { Collection<String> localExcludes = excludes; - if (!copyErrors || !copyMessages ||!copyFieldErrors) { + if (!copyErrors || !copyMessages || !copyFieldErrors) { if (localExcludes == null) { localExcludes = new HashSet<>(); if (!copyErrors) { diff --git a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java index 32cffc291..293f4968a 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java @@ -39,7 +39,7 @@ import org.apache.struts2.security.DefaultAcceptedPatternsChecker; import org.apache.struts2.security.ExcludedPatternsChecker; import org.apache.struts2.util.ClearableValueStack; import org.apache.struts2.util.MemberAccessValueStack; -import org.apache.struts2.util.ProxyUtil; +import org.apache.struts2.util.ProxyService; import org.apache.struts2.util.TextParseUtil; import org.apache.struts2.util.ValueStack; import org.apache.struts2.util.ValueStackFactory; @@ -95,6 +95,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor { private ValueStackFactory valueStackFactory; private OgnlUtil ognlUtil; protected ThreadAllowlist threadAllowlist; + private ProxyService proxyService; private ExcludedPatternsChecker excludedPatterns; private AcceptedPatternsChecker acceptedPatterns; private Set<Pattern> excludedValuePatterns = null; @@ -115,6 +116,11 @@ public class ParametersInterceptor extends MethodFilterInterceptor { this.threadAllowlist = threadAllowlist; } + @Inject + public void setProxyService(ProxyService proxyService) { + this.proxyService = proxyService; + } + @Inject(StrutsConstants.STRUTS_DEVMODE) public void setDevMode(String mode) { this.devMode = BooleanUtils.toBoolean(mode); @@ -516,8 +522,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor { } protected Class<?> ultimateClass(Object action) { - if (ProxyUtil.isProxy(action)) { - return ProxyUtil.ultimateTargetClass(action); + if (proxyService.isProxy(action)) { + return proxyService.ultimateTargetClass(action); } return action.getClass(); } diff --git a/core/src/main/java/org/apache/struts2/ognl/ProxyCacheFactory.java b/core/src/main/java/org/apache/struts2/ognl/ProxyCacheFactory.java new file mode 100644 index 000000000..1243b6f24 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/ognl/ProxyCacheFactory.java @@ -0,0 +1,27 @@ +/* + * Copyright 2022 Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.ognl; + +/** + * A proxy interface to be used with Struts DI mechanism for proxy detection caching. + * + * @param <Key> The type for the cache key entries + * @param <Value> The type for the cache value entries + * @since 7.2.0 + */ +public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> { + +} diff --git a/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java b/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java index 035a685bf..d25bbe377 100644 --- a/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java +++ b/core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java @@ -25,7 +25,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.StrutsConstants; import org.apache.struts2.inject.Inject; -import org.apache.struts2.util.ProxyUtil; +import org.apache.struts2.util.ProxyService; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; @@ -76,6 +76,8 @@ public class SecurityMemberAccess implements MemberAccess { private final ProviderAllowlist providerAllowlist; private final ThreadAllowlist threadAllowlist; + private ProxyService proxyService; + private boolean allowStaticFieldAccess = true; private Set<Pattern> excludeProperties = emptySet(); @@ -107,6 +109,11 @@ public class SecurityMemberAccess implements MemberAccess { this.threadAllowlist = threadAllowlist; } + @Inject + public void setProxyService(ProxyService proxyService) { + this.proxyService = proxyService; + } + @Override public Object setup(OgnlContext context, Object target, Member member, String propertyName) { Object result = null; @@ -214,15 +221,15 @@ public class SecurityMemberAccess implements MemberAccess { Class<?> targetClass = target != null ? target.getClass() : null; - if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) { + if (!disallowProxyObjectAccess && proxyService.isProxy(target)) { // If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities and Spring proxies to their // underlying classes/members. This allows the allowlist capability to continue working and still offer // protection in applications where the developer has accepted the risk of allowing OGNL access to Hibernate // entities and Spring proxies. This is preferred to having to disable the allowlist capability entirely. - Class<?> newTargetClass = ProxyUtil.ultimateTargetClass(target); + Class<?> newTargetClass = proxyService.ultimateTargetClass(target); if (newTargetClass != targetClass) { targetClass = newTargetClass; - member = ProxyUtil.resolveTargetMember(member, newTargetClass); + member = proxyService.resolveTargetMember(member, newTargetClass); } } @@ -312,14 +319,14 @@ public class SecurityMemberAccess implements MemberAccess { * @return {@code true} if proxy object access is allowed */ protected boolean checkProxyObjectAccess(Object target) { - return !(disallowProxyObjectAccess && ProxyUtil.isProxy(target)); + return !(disallowProxyObjectAccess && proxyService.isProxy(target)); } /** * @return {@code true} if proxy member access is allowed */ protected boolean checkProxyMemberAccess(Object target, Member member) { - return !(disallowProxyMemberAccess && ProxyUtil.isProxyMember(member, target)); + return !(disallowProxyMemberAccess && proxyService.isProxyMember(member, target)); } /** diff --git a/core/src/main/java/org/apache/struts2/ognl/StrutsProxyCacheFactory.java b/core/src/main/java/org/apache/struts2/ognl/StrutsProxyCacheFactory.java new file mode 100644 index 000000000..ac80163af --- /dev/null +++ b/core/src/main/java/org/apache/struts2/ognl/StrutsProxyCacheFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.struts2.ognl; + +import org.apache.commons.lang3.EnumUtils; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.inject.Inject; + +/** + * Struts proxy cache factory implementation. + * Used for creating caches for proxy detection operations. + * + * @param <Key> The type for the cache key entries + * @param <Value> The type for the cache value entries + * @since 7.2.0 + */ +public class StrutsProxyCacheFactory<Key, Value> extends DefaultOgnlCacheFactory<Key, Value> + implements ProxyCacheFactory<Key, Value> { + + @Inject + public StrutsProxyCacheFactory( + @Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize, + @Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) { + super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType)); + } +} diff --git a/core/src/main/java/org/apache/struts2/util/ProxyService.java b/core/src/main/java/org/apache/struts2/util/ProxyService.java new file mode 100644 index 000000000..fe6aca7ae --- /dev/null +++ b/core/src/main/java/org/apache/struts2/util/ProxyService.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.util; + +import java.lang.reflect.Member; + +/** + * Service interface for proxy detection and resolution operations. + * Replaces static {@link ProxyUtil} methods with an injectable service. + * + * @since 7.2.0 + */ +public interface ProxyService { + + /** + * Determine the ultimate target class of the given instance, traversing + * not only a top-level proxy but any number of nested proxies as well &mdash; + * as long as possible without side effects. + * + * @param candidate the instance to check (might be a proxy) + * @return the ultimate target class (or the plain class of the given + * object as fallback; never {@code null}) + */ + Class<?> ultimateTargetClass(Object candidate); + + /** + * Check whether the given object is a proxy. + * + * @param object the object to check + * @return true if the object is a Spring AOP or Hibernate proxy + */ + boolean isProxy(Object object); + + /** + * Check whether the given member is a proxy member of a proxy object or is a static proxy member. + * + * @param member the member to check + * @param object the object to check + * @return true if the member is a proxy member + */ + boolean isProxyMember(Member member, Object object); + + /** + * Check whether the given object is a Hibernate proxy. + * + * @param object the object to check + * @return true if the object is a Hibernate proxy + */ + boolean isHibernateProxy(Object object); + + /** + * Check whether the given member is a member of a Hibernate proxy. + * + * @param member the member to check + * @return true if the member is a Hibernate proxy member + */ + boolean isHibernateProxyMember(Member member); + + /** + * Get the target instance of the given object if it is a Hibernate proxy object, + * otherwise return the given object. + * + * @param object the object to check + * @return the target instance or the original object + */ + Object getHibernateProxyTarget(Object object); + + /** + * Resolve matching member on target class. + * + * @param proxyMember the proxy member + * @param targetClass the target class + * @return matching member on target object if one exists, otherwise the same member + */ + Member resolveTargetMember(Member proxyMember, Class<?> targetClass); + + /** + * @param proxyMember the proxy member + * @param target the target object + * @return matching member on target object if one exists, otherwise the same member + * @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead. + */ + @Deprecated + Member resolveTargetMember(Member proxyMember, Object target); +} diff --git a/core/src/main/java/org/apache/struts2/util/ProxyUtil.java b/core/src/main/java/org/apache/struts2/util/ProxyUtil.java index a574af1c2..1260a7dee 100644 --- a/core/src/main/java/org/apache/struts2/util/ProxyUtil.java +++ b/core/src/main/java/org/apache/struts2/util/ProxyUtil.java @@ -43,10 +43,12 @@ import static java.lang.reflect.Modifier.isStatic; /** * <code>ProxyUtil</code> * <p> - * Various utility methods dealing with proxies + * Various utility methods dealing with proxies. * </p> * + * @deprecated since 7.2, inject {@link ProxyService} instead. This class will be removed in a future version. */ +@Deprecated(since = "7.2") public class ProxyUtil { private static final int CACHE_MAX_SIZE = 10000; private static final int CACHE_INITIAL_CAPACITY = 256; @@ -61,10 +63,13 @@ public class ProxyUtil { * Determine the ultimate target class of the given instance, traversing * not only a top-level proxy but any number of nested proxies as well — * as long as possible without side effects. + * * @param candidate the instance to check (might be a proxy) * @return the ultimate target class (or the plain class of the given * object as fallback; never {@code null}) + * @deprecated since 7.2, inject {@link ProxyService} instead */ + @Deprecated(since = "7.2") public static Class<?> ultimateTargetClass(Object candidate) { return targetClassCache.computeIfAbsent(candidate, k -> { Class<?> result = null; @@ -82,8 +87,12 @@ public class ProxyUtil { /** * Check whether the given object is a proxy. + * * @param object the object to check + * @return true if the object is a Spring AOP or Hibernate proxy + * @deprecated since 7.2, inject {@link ProxyService} instead */ + @Deprecated(since = "7.2") public static boolean isProxy(Object object) { if (object == null) return false; return isProxyCache.computeIfAbsent(object.getClass(), @@ -92,9 +101,13 @@ public class ProxyUtil { /** * Check whether the given member is a proxy member of a proxy object or is a static proxy member. + * * @param member the member to check * @param object the object to check + * @return true if the member is a proxy member + * @deprecated since 7.2, inject {@link ProxyService} instead */ + @Deprecated(since = "7.2") public static boolean isProxyMember(Member member, Object object) { if (!isStatic(member.getModifiers()) && !isProxy(object)) { return false; @@ -107,7 +120,10 @@ public class ProxyUtil { * Check whether the given object is a Hibernate proxy. * * @param object the object to check + * @return true if the object is a Hibernate proxy + * @deprecated since 7.2, inject {@link ProxyService} instead */ + @Deprecated(since = "7.2") public static boolean isHibernateProxy(Object object) { try { return object != null && HibernateProxy.class.isAssignableFrom(object.getClass()); @@ -120,7 +136,10 @@ public class ProxyUtil { * Check whether the given member is a member of a Hibernate proxy. * * @param member the member to check + * @return true if the member is a Hibernate proxy member + * @deprecated since 7.2, inject {@link ProxyService} instead */ + @Deprecated(since = "7.2") public static boolean isHibernateProxyMember(Member member) { try { return hasMember(HibernateProxy.class, member); @@ -133,6 +152,7 @@ public class ProxyUtil { * Determine the ultimate target class of the given spring bean instance, traversing * not only a top-level spring proxy but any number of nested spring proxies as well — * as long as possible without side effects, that is, just for singleton targets. + * * @param candidate the instance to check (might be a spring AOP proxy) * @return the ultimate target class (or the plain class of the given * object as fallback; never {@code null}) @@ -147,6 +167,7 @@ public class ProxyUtil { /** * Check whether the given object is a Spring proxy. + * * @param object the object to check */ private static boolean isSpringAopProxy(Object object) { @@ -159,6 +180,7 @@ public class ProxyUtil { /** * Check whether the given member is a member of a spring proxy. + * * @param member the member to check */ private static boolean isSpringProxyMember(Member member) { @@ -176,7 +198,8 @@ public class ProxyUtil { /** * Check whether the given class has a given member. - * @param clazz the class to check + * + * @param clazz the class to check * @param member the member to check */ private static boolean hasMember(Class<?> clazz, Member member) { @@ -193,8 +216,13 @@ public class ProxyUtil { } /** + * Get the target instance of the given object if it is a Hibernate proxy object. + * + * @param object the object to check * @return the target instance of the given object if it is a Hibernate proxy object, otherwise the given object + * @deprecated since 7.2, inject {@link ProxyService} instead */ + @Deprecated(since = "7.2") public static Object getHibernateProxyTarget(Object object) { try { return Hibernate.unproxy(object); @@ -204,9 +232,15 @@ public class ProxyUtil { } /** + * Resolve matching member on target object. + * + * @param proxyMember the proxy member + * @param target the target object + * @return matching member on target object if one exists, otherwise the same member * @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead. + * Since 7.2, inject {@link ProxyService} instead. */ - @Deprecated + @Deprecated(since = "7.1") public static Member resolveTargetMember(Member proxyMember, Object target) { return resolveTargetMember(proxyMember, target.getClass()); } diff --git a/core/src/main/java/org/apache/struts2/util/ProxyUtil.java b/core/src/main/java/org/apache/struts2/util/StrutsProxyService.java similarity index 59% copy from core/src/main/java/org/apache/struts2/util/ProxyUtil.java copy to core/src/main/java/org/apache/struts2/util/StrutsProxyService.java index a574af1c2..8d3e54798 100644 --- a/core/src/main/java/org/apache/struts2/util/ProxyUtil.java +++ b/core/src/main/java/org/apache/struts2/util/StrutsProxyService.java @@ -21,16 +21,16 @@ package org.apache.struts2.util; import org.apache.commons.lang3.reflect.ConstructorUtils; import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.commons.lang3.reflect.MethodUtils; -import org.apache.struts2.ognl.DefaultOgnlCacheFactory; +import org.apache.struts2.inject.Inject; import org.apache.struts2.ognl.OgnlCache; -import org.apache.struts2.ognl.OgnlCacheFactory; +import org.apache.struts2.ognl.ProxyCacheFactory; import org.hibernate.Hibernate; import org.hibernate.proxy.HibernateProxy; -import org.springframework.aop.SpringProxy; import org.springframework.aop.TargetClassAware; import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.support.AopUtils; +import org.springframework.aop.SpringProxy; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -41,31 +41,27 @@ import static java.lang.reflect.Modifier.isPublic; import static java.lang.reflect.Modifier.isStatic; /** - * <code>ProxyUtil</code> - * <p> - * Various utility methods dealing with proxies - * </p> + * Default implementation of {@link ProxyService}. + * Provides proxy detection and resolution for Spring AOP and Hibernate proxies. * + * @since 7.2.0 */ -public class ProxyUtil { - private static final int CACHE_MAX_SIZE = 10000; - private static final int CACHE_INITIAL_CAPACITY = 256; - private static final OgnlCache<Class<?>, Boolean> isProxyCache = new DefaultOgnlCacheFactory<Class<?>, Boolean>( - CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache(); - private static final OgnlCache<Member, Boolean> isProxyMemberCache = new DefaultOgnlCacheFactory<Member, Boolean>( - CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache(); - private static final OgnlCache<Object, Class<?>> targetClassCache = new DefaultOgnlCacheFactory<Object, Class<?>>( - CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache(); +public class StrutsProxyService implements ProxyService { + + private final OgnlCache<Class<?>, Boolean> isProxyCache; + private final OgnlCache<Member, Boolean> isProxyMemberCache; + private final OgnlCache<Object, Class<?>> targetClassCache; + + @Inject + @SuppressWarnings("unchecked") + public StrutsProxyService(ProxyCacheFactory<?, ?> proxyCacheFactory) { + this.isProxyCache = (OgnlCache<Class<?>, Boolean>) proxyCacheFactory.buildOgnlCache(); + this.isProxyMemberCache = (OgnlCache<Member, Boolean>) proxyCacheFactory.buildOgnlCache(); + this.targetClassCache = (OgnlCache<Object, Class<?>>) proxyCacheFactory.buildOgnlCache(); + } - /** - * Determine the ultimate target class of the given instance, traversing - * not only a top-level proxy but any number of nested proxies as well — - * as long as possible without side effects. - * @param candidate the instance to check (might be a proxy) - * @return the ultimate target class (or the plain class of the given - * object as fallback; never {@code null}) - */ - public static Class<?> ultimateTargetClass(Object candidate) { + @Override + public Class<?> ultimateTargetClass(Object candidate) { return targetClassCache.computeIfAbsent(candidate, k -> { Class<?> result = null; if (isSpringAopProxy(k)) { @@ -80,22 +76,15 @@ public class ProxyUtil { }); } - /** - * Check whether the given object is a proxy. - * @param object the object to check - */ - public static boolean isProxy(Object object) { + @Override + public boolean isProxy(Object object) { if (object == null) return false; return isProxyCache.computeIfAbsent(object.getClass(), k -> isSpringAopProxy(object) || isHibernateProxy(object)); } - /** - * Check whether the given member is a proxy member of a proxy object or is a static proxy member. - * @param member the member to check - * @param object the object to check - */ - public static boolean isProxyMember(Member member, Object object) { + @Override + public boolean isProxyMember(Member member, Object object) { if (!isStatic(member.getModifiers()) && !isProxy(object)) { return false; } @@ -103,12 +92,8 @@ public class ProxyUtil { k -> isSpringProxyMember(member) || isHibernateProxyMember(member)); } - /** - * Check whether the given object is a Hibernate proxy. - * - * @param object the object to check - */ - public static boolean isHibernateProxy(Object object) { + @Override + public boolean isHibernateProxy(Object object) { try { return object != null && HibernateProxy.class.isAssignableFrom(object.getClass()); } catch (LinkageError ignored) { @@ -116,12 +101,8 @@ public class ProxyUtil { } } - /** - * Check whether the given member is a member of a Hibernate proxy. - * - * @param member the member to check - */ - public static boolean isHibernateProxyMember(Member member) { + @Override + public boolean isHibernateProxyMember(Member member) { try { return hasMember(HibernateProxy.class, member); } catch (LinkageError ignored) { @@ -129,15 +110,42 @@ public class ProxyUtil { } } + @Override + public Object getHibernateProxyTarget(Object object) { + try { + return Hibernate.unproxy(object); + } catch (LinkageError ignored) { + return object; + } + } + + @Override + public Member resolveTargetMember(Member proxyMember, Class<?> targetClass) { + int mod = proxyMember.getModifiers(); + if (proxyMember instanceof Method) { + if (isPublic(mod)) { + return MethodUtils.getMatchingAccessibleMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes()); + } else { + return MethodUtils.getMatchingMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes()); + } + } else if (proxyMember instanceof Field) { + return FieldUtils.getField(targetClass, proxyMember.getName(), isPublic(mod)); + } else if (proxyMember instanceof Constructor && isPublic(mod)) { + return ConstructorUtils.getMatchingAccessibleConstructor(targetClass, ((Constructor<?>) proxyMember).getParameterTypes()); + } + return proxyMember; + } + + @Override + @Deprecated + public Member resolveTargetMember(Member proxyMember, Object target) { + return resolveTargetMember(proxyMember, target.getClass()); + } + /** - * Determine the ultimate target class of the given spring bean instance, traversing - * not only a top-level spring proxy but any number of nested spring proxies as well — - * as long as possible without side effects, that is, just for singleton targets. - * @param candidate the instance to check (might be a spring AOP proxy) - * @return the ultimate target class (or the plain class of the given - * object as fallback; never {@code null}) + * Determine the ultimate target class of the given spring bean instance. */ - private static Class<?> springUltimateTargetClass(Object candidate) { + private Class<?> springUltimateTargetClass(Object candidate) { try { return AopProxyUtils.ultimateTargetClass(candidate); } catch (LinkageError ignored) { @@ -147,9 +155,8 @@ public class ProxyUtil { /** * Check whether the given object is a Spring proxy. - * @param object the object to check */ - private static boolean isSpringAopProxy(Object object) { + private boolean isSpringAopProxy(Object object) { try { return AopUtils.isAopProxy(object); } catch (LinkageError ignored) { @@ -159,9 +166,8 @@ public class ProxyUtil { /** * Check whether the given member is a member of a spring proxy. - * @param member the member to check */ - private static boolean isSpringProxyMember(Member member) { + private boolean isSpringProxyMember(Member member) { try { if (hasMember(Advised.class, member)) return true; @@ -176,10 +182,8 @@ public class ProxyUtil { /** * Check whether the given class has a given member. - * @param clazz the class to check - * @param member the member to check */ - private static boolean hasMember(Class<?> clazz, Member member) { + private boolean hasMember(Class<?> clazz, Member member) { if (member instanceof Method method) { return null != MethodUtils.getMatchingMethod(clazz, member.getName(), method.getParameterTypes()); } @@ -191,42 +195,4 @@ public class ProxyUtil { } return false; } - - /** - * @return the target instance of the given object if it is a Hibernate proxy object, otherwise the given object - */ - public static Object getHibernateProxyTarget(Object object) { - try { - return Hibernate.unproxy(object); - } catch (LinkageError ignored) { - return object; - } - } - - /** - * @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead. - */ - @Deprecated - public static Member resolveTargetMember(Member proxyMember, Object target) { - return resolveTargetMember(proxyMember, target.getClass()); - } - - /** - * @return matching member on target object if one exists, otherwise the same member - */ - public static Member resolveTargetMember(Member proxyMember, Class<?> targetClass) { - int mod = proxyMember.getModifiers(); - if (proxyMember instanceof Method) { - if (isPublic(mod)) { - return MethodUtils.getMatchingAccessibleMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes()); - } else { - return MethodUtils.getMatchingMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes()); - } - } else if (proxyMember instanceof Field) { - return FieldUtils.getField(targetClass, proxyMember.getName(), isPublic(mod)); - } else if (proxyMember instanceof Constructor && isPublic(mod)) { - return ConstructorUtils.getMatchingAccessibleConstructor(targetClass, ((Constructor<?>) proxyMember).getParameterTypes()); - } - return proxyMember; - } } diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index a980196a6..eb841d783 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -283,6 +283,19 @@ struts.ognl.beanInfoCacheType=wtlfu ### application-specific needs. struts.ognl.beanInfoCacheMaxSize=10000 +### Specifies the type of cache to use for proxy detection. See StrutsConstants class for further information. +### Using 'basic' by default to avoid mandatory Caffeine dependency. +struts.proxy.cacheType=basic + +### Specifies the maximum cache size for proxy detection caches. +struts.proxy.cacheMaxSize=10000 + +### Specifies the ProxyCacheFactory implementation class. +struts.proxy.cacheFactory=struts + +### Specifies the ProxyService implementation class. +struts.proxyService=struts + ### Indicates if Dispatcher should handle unexpected exceptions by calling sendError() ### or simply rethrow it as a ServletException to allow future processing by other frameworks like Spring Security struts.handle.exception=true diff --git a/core/src/main/resources/struts-beans.xml b/core/src/main/resources/struts-beans.xml index 742d5634f..7c59a88da 100644 --- a/core/src/main/resources/struts-beans.xml +++ b/core/src/main/resources/struts-beans.xml @@ -240,6 +240,10 @@ class="org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/> <bean type="org.apache.struts2.ognl.BeanInfoCacheFactory" name="struts" class="org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/> + <bean type="org.apache.struts2.ognl.ProxyCacheFactory" name="struts" + class="org.apache.struts2.ognl.StrutsProxyCacheFactory" scope="singleton"/> + <bean type="org.apache.struts2.util.ProxyService" name="struts" + class="org.apache.struts2.util.StrutsProxyService" scope="singleton"/> <bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder" class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/> diff --git a/core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java b/core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java index 54125fdef..0040e98d3 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/parameter/StrutsParameterAnnotationTest.java @@ -30,7 +30,9 @@ import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory; import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory; import org.apache.struts2.ognl.OgnlUtil; import org.apache.struts2.ognl.StrutsOgnlGuard; +import org.apache.struts2.ognl.StrutsProxyCacheFactory; import org.apache.struts2.ognl.ThreadAllowlist; +import org.apache.struts2.util.StrutsProxyService; import org.apache.struts2.security.AcceptedPatternsChecker.IsAccepted; import org.apache.struts2.security.ExcludedPatternsChecker.IsExcluded; import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker; @@ -71,6 +73,9 @@ public class StrutsParameterAnnotationTest { new StrutsOgnlGuard()); parametersInterceptor.setOgnlUtil(ognlUtil); + var proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); + parametersInterceptor.setProxyService(proxyService); + NotExcludedAcceptedPatternsChecker checker = mock(NotExcludedAcceptedPatternsChecker.class); when(checker.isAccepted(anyString())).thenReturn(IsAccepted.yes("")); when(checker.isExcluded(anyString())).thenReturn(IsExcluded.no(Set.of())); diff --git a/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessTest.java b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessTest.java index 371e39aa4..a9b7b8c12 100644 --- a/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessTest.java +++ b/core/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessTest.java @@ -24,7 +24,9 @@ import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.struts2.TestBean; import org.apache.struts2.config.ConfigurationException; import org.apache.struts2.test.TestBean2; +import org.apache.struts2.util.StrutsProxyService; import org.apache.struts2.util.Foo; +import org.apache.struts2.util.ProxyService; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.junit.Before; @@ -58,6 +60,7 @@ public class SecurityMemberAccessTest { protected SecurityMemberAccess sma; protected ProviderAllowlist mockedProviderAllowlist; protected ThreadAllowlist mockedThreadAllowlist; + protected ProxyService proxyService; @Before public void setUp() { @@ -65,6 +68,7 @@ public class SecurityMemberAccessTest { target = new FooBar(); mockedProviderAllowlist = mock(ProviderAllowlist.class); mockedThreadAllowlist = mock(ThreadAllowlist.class); + proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); assignNewSma(true); } @@ -77,6 +81,7 @@ public class SecurityMemberAccessTest { protected void assignNewSmaHelper() { sma = new SecurityMemberAccess(mockedProviderAllowlist, mockedThreadAllowlist); + sma.setProxyService(proxyService); } private <T> T reflectField(String fieldName) throws IllegalAccessException { diff --git a/core/src/test/java/org/apache/struts2/ognl/StrutsProxyCacheFactoryTest.java b/core/src/test/java/org/apache/struts2/ognl/StrutsProxyCacheFactoryTest.java new file mode 100644 index 000000000..52f8fdf5f --- /dev/null +++ b/core/src/test/java/org/apache/struts2/ognl/StrutsProxyCacheFactoryTest.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.ognl; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StrutsProxyCacheFactory}. + */ +public class StrutsProxyCacheFactoryTest { + + @Test + public void testCreateBasicCache() { + StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("1000", "basic"); + + OgnlCache<String, Boolean> cache = factory.buildOgnlCache(); + + assertThat(cache).isNotNull(); + assertThat(cache).isInstanceOf(OgnlDefaultCache.class); + assertThat(cache.getEvictionLimit()).isEqualTo(1000); + } + + @Test + public void testCreateLruCache() { + StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("500", "lru"); + + OgnlCache<String, Boolean> cache = factory.buildOgnlCache(); + + assertThat(cache).isNotNull(); + assertThat(cache).isInstanceOf(OgnlLRUCache.class); + assertThat(cache.getEvictionLimit()).isEqualTo(500); + } + + @Test + public void testCreateWtlfuCache() { + StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("2000", "wtlfu"); + + OgnlCache<String, Boolean> cache = factory.buildOgnlCache(); + + assertThat(cache).isNotNull(); + assertThat(cache).isInstanceOf(OgnlCaffeineCache.class); + assertThat(cache.getEvictionLimit()).isEqualTo(2000); + } + + @Test + public void testCacheTypeIgnoresCase() { + StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("1000", "BASIC"); + + OgnlCache<String, Boolean> cache = factory.buildOgnlCache(); + + assertThat(cache).isInstanceOf(OgnlDefaultCache.class); + } + + @Test + public void testGetCacheMaxSize() { + StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("5000", "basic"); + + assertThat(factory.getCacheMaxSize()).isEqualTo(5000); + } + + @Test + public void testGetDefaultCacheType() { + StrutsProxyCacheFactory<String, Boolean> factory = new StrutsProxyCacheFactory<>("1000", "lru"); + + assertThat(factory.getDefaultCacheType()).isEqualTo(OgnlCacheFactory.CacheType.LRU); + } +} diff --git a/core/src/test/java/org/apache/struts2/util/StrutsProxyServiceTest.java b/core/src/test/java/org/apache/struts2/util/StrutsProxyServiceTest.java new file mode 100644 index 000000000..94d525b7c --- /dev/null +++ b/core/src/test/java/org/apache/struts2/util/StrutsProxyServiceTest.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.util; + +import org.apache.struts2.ognl.StrutsProxyCacheFactory; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StrutsProxyService}. + */ +public class StrutsProxyServiceTest { + + private StrutsProxyService proxyService; + + @Before + public void setUp() { + StrutsProxyCacheFactory<?, ?> factory = new StrutsProxyCacheFactory<>("1000", "basic"); + proxyService = new StrutsProxyService(factory); + } + + @Test + public void testIsProxyWithNull() { + assertThat(proxyService.isProxy(null)).isFalse(); + } + + @Test + public void testIsProxyWithRegularObject() { + Object regularObject = new Object(); + assertThat(proxyService.isProxy(regularObject)).isFalse(); + } + + @Test + public void testIsProxyWithString() { + String str = "test"; + assertThat(proxyService.isProxy(str)).isFalse(); + } + + @Test + public void testUltimateTargetClassWithRegularObject() { + Object regularObject = new Object(); + Class<?> targetClass = proxyService.ultimateTargetClass(regularObject); + assertThat(targetClass).isEqualTo(Object.class); + } + + @Test + public void testUltimateTargetClassWithString() { + String str = "test"; + Class<?> targetClass = proxyService.ultimateTargetClass(str); + assertThat(targetClass).isEqualTo(String.class); + } + + @Test + public void testIsHibernateProxyWithNull() { + assertThat(proxyService.isHibernateProxy(null)).isFalse(); + } + + @Test + public void testIsHibernateProxyWithRegularObject() { + Object regularObject = new Object(); + assertThat(proxyService.isHibernateProxy(regularObject)).isFalse(); + } + + @Test + public void testIsProxyMemberWithNonProxy() throws NoSuchMethodException { + Object regularObject = new Object(); + Method method = Object.class.getMethod("toString"); + assertThat(proxyService.isProxyMember(method, regularObject)).isFalse(); + } + + @Test + public void testResolveTargetMemberReturnsMethodOnTargetClass() throws NoSuchMethodException { + Method toStringMethod = Object.class.getMethod("toString"); + Member resolved = proxyService.resolveTargetMember(toStringMethod, String.class); + + assertThat(resolved).isNotNull(); + assertThat(resolved.getName()).isEqualTo("toString"); + assertThat(resolved.getDeclaringClass()).isEqualTo(String.class); + } + + @Test + public void testResolveTargetMemberDeprecatedMethod() throws NoSuchMethodException { + Method toStringMethod = Object.class.getMethod("toString"); + String target = "test"; + + @SuppressWarnings("deprecation") + Member resolved = proxyService.resolveTargetMember(toStringMethod, target); + + assertThat(resolved).isNotNull(); + assertThat(resolved.getName()).isEqualTo("toString"); + } + + @Test + public void testGetHibernateProxyTargetWithRegularObject() { + Object regularObject = new Object(); + Object result = proxyService.getHibernateProxyTarget(regularObject); + assertThat(result).isSameAs(regularObject); + } +} diff --git a/core/src/test/java/org/test/ExternalSecurityMemberAccessTest.java b/core/src/test/java/org/test/ExternalSecurityMemberAccessTest.java index 430979057..5a1edc25b 100644 --- a/core/src/test/java/org/test/ExternalSecurityMemberAccessTest.java +++ b/core/src/test/java/org/test/ExternalSecurityMemberAccessTest.java @@ -29,5 +29,6 @@ public class ExternalSecurityMemberAccessTest extends SecurityMemberAccessTest { @Override protected void assignNewSmaHelper() { sma = new ExternalSecurityMemberAccess(mockedProviderAllowlist, mockedThreadAllowlist); + sma.setProxyService(proxyService); } } diff --git a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java b/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java index 8e768bd91..df4ba2dec 100644 --- a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java +++ b/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java @@ -26,7 +26,7 @@ import org.apache.struts2.json.annotations.JSONFieldBridge; import org.apache.struts2.json.annotations.JSONParameter; import org.apache.struts2.json.bridge.FieldBridge; import org.apache.struts2.json.bridge.ParameterizedBridge; -import org.apache.struts2.util.ProxyUtil; +import org.apache.struts2.util.ProxyService; import java.beans.BeanInfo; import java.beans.IntrospectionException; @@ -78,6 +78,12 @@ public class DefaultJSONWriter implements JSONWriter { private boolean excludeNullProperties; private boolean cacheBeanInfo = true; private boolean excludeProxyProperties; + private ProxyService proxyService; + + @Inject + public void setProxyService(ProxyService proxyService) { + this.proxyService = proxyService; + } @Inject(value = JSONConstants.RESULT_EXCLUDE_PROXY_PROPERTIES, required = false) public void setExcludeProxyProperties(String excludeProxyProperties) { @@ -221,7 +227,7 @@ public class DefaultJSONWriter implements JSONWriter { BeanInfo info; try { - Class<?> clazz = excludeProxyProperties ? ProxyUtil.ultimateTargetClass(object) : object.getClass(); + Class<?> clazz = excludeProxyProperties ? proxyService.ultimateTargetClass(object) : object.getClass(); info = ((object == this.root) && this.ignoreHierarchy) ? getBeanInfoIgnoreHierarchy(clazz) diff --git a/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java b/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java index 78ee5535b..77831ed94 100644 --- a/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java +++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java @@ -24,7 +24,10 @@ import org.apache.struts2.StrutsStatics; import org.apache.struts2.junit.StrutsTestCase; import org.apache.struts2.junit.util.TestUtils; import org.apache.struts2.mock.MockActionInvocation; +import org.apache.struts2.ognl.StrutsProxyCacheFactory; import org.apache.struts2.result.Result; +import org.apache.struts2.util.StrutsProxyService; +import org.apache.struts2.util.ProxyService; import org.apache.struts2.util.ValueStack; import org.springframework.aop.framework.ProxyFactory; import org.springframework.mock.web.MockHttpServletRequest; @@ -56,6 +59,7 @@ public class JSONResultTest extends StrutsTestCase { ActionContext context; ValueStack stack; MockHttpServletRequest request; + ProxyService proxyService; public void testJSONUtilNPEOnNullMehtod() { Map map = new HashMap(); @@ -157,7 +161,8 @@ public class JSONResultTest extends StrutsTestCase { public void testNotTraverseOrIncludeProxyInfo() throws Exception { JSONResult result = new JSONResult(); JSONUtil jsonUtil = new JSONUtil(); - JSONWriter writer = new DefaultJSONWriter(); + DefaultJSONWriter writer = new DefaultJSONWriter(); + writer.setProxyService(proxyService); jsonUtil.setWriter(writer); result.setJsonUtil(jsonUtil); Object proxiedAction = new ProxyFactory(new TestAction2()).getProxy(); @@ -737,5 +742,6 @@ public class JSONResultTest extends StrutsTestCase { this.invocation = new MockActionInvocation(); this.invocation.setInvocationContext(this.context); this.invocation.setStack(this.stack); + this.proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); } } diff --git a/plugins/spring/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessProxyTest.java b/plugins/spring/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessProxyTest.java index c02dc5cd3..526d528ab 100644 --- a/plugins/spring/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessProxyTest.java +++ b/plugins/spring/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessProxyTest.java @@ -22,6 +22,8 @@ import org.apache.struts2.ActionProxy; import org.apache.struts2.XWorkJUnit4TestCase; import org.apache.struts2.config.StrutsXmlConfigurationProvider; import org.apache.struts2.config.providers.XmlConfigurationProvider; +import org.apache.struts2.util.StrutsProxyService; +import org.apache.struts2.util.ProxyService; import org.junit.Before; import org.junit.Test; import org.springframework.aop.MethodBeforeAdvice; @@ -43,7 +45,8 @@ public class SecurityMemberAccessProxyTest extends XWorkJUnit4TestCase { private OgnlContext context; private ActionProxy proxy; - private final SecurityMemberAccess sma = new SecurityMemberAccess(null, null); + private SecurityMemberAccess sma; + private ProxyService proxyService; private Member proxyObjectProxyMember; private Member proxyObjectNonProxyMember; @@ -58,6 +61,10 @@ public class SecurityMemberAccessProxyTest extends XWorkJUnit4TestCase { proxy = actionProxyFactory.createActionProxy(null, "chaintoAOPedTestSubBeanAction", null, context); proxyObjectProxyMember = proxy.getAction().getClass().getMethod(PROXY_MEMBER_METHOD); proxyObjectNonProxyMember = proxy.getAction().getClass().getMethod(TEST_SUB_BEAN_CLASS_METHOD); + + proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); + sma = new SecurityMemberAccess(null, null); + sma.setProxyService(proxyService); } /** diff --git a/thoughts/shared/research/2026-02-07-WW-5514-proxy-cache-configuration.md b/thoughts/shared/research/2026-02-07-WW-5514-proxy-cache-configuration.md new file mode 100644 index 000000000..c65cdc063 --- /dev/null +++ b/thoughts/shared/research/2026-02-07-WW-5514-proxy-cache-configuration.md @@ -0,0 +1,372 @@ +--- +date: 2026-02-07T12:00:00+01:00 +topic: "WW-5514: Allow Configuration of ProxyUtil Cache Types" +tags: [research, implementation-plan, proxy, cache, caffeine, WW-5514] +status: complete +--- + +# WW-5514: Allow Configuration of ProxyUtil Cache Types + +**Date**: 2026-02-07 +**JIRA**: https://issues.apache.org/jira/browse/WW-5514 + +## Problem Statement + +`ProxyUtil` hardcodes `CacheType.WTLFU` for its internal caches, making the Caffeine library mandatory. Users need the ability to configure cache types (e.g., `BASIC`) to avoid this dependency. + +### Current Implementation + +**File**: `core/src/main/java/org/apache/struts2/util/ProxyUtil.java` + +```java +private static final OgnlCache<Class<?>, Boolean> isProxyCache = + new DefaultOgnlCacheFactory<>(CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache(); +``` + +Three static caches are hardcoded with WTLFU: +- `isProxyCache` - Caches proxy class detection +- `isProxyMemberCache` - Caches proxy member detection +- `targetClassCache` - Caches ultimate target class resolution + +--- + +## Solution: Option A - Injectable ProxyService + +Refactor `ProxyUtil` from a static utility to an injectable service following the `OgnlUtil`/`ExpressionCacheFactory` pattern. + +--- + +## Files to Create + +### 1. `core/src/main/java/org/apache/struts2/ognl/ProxyCacheFactory.java` + +Marker interface extending `OgnlCacheFactory` for DI. + +```java +package org.apache.struts2.ognl; + +/** + * A proxy interface to be used with Struts DI mechanism for proxy detection caching. + * + * @since 7.2.0 + */ +public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> { +} +``` + +### 2. `core/src/main/java/org/apache/struts2/ognl/StrutsProxyCacheFactory.java` + +Implementation with `@Inject` constructor taking configuration constants. + +```java +package org.apache.struts2.ognl; + +import org.apache.commons.lang3.EnumUtils; +import org.apache.struts2.StrutsConstants; +import org.apache.struts2.inject.Inject; + +/** + * Struts proxy cache factory implementation. + * Used for creating caches for proxy detection operations. + * + * @since 7.2.0 + */ +public class StrutsProxyCacheFactory<Key, Value> extends DefaultOgnlCacheFactory<Key, Value> + implements ProxyCacheFactory<Key, Value> { + + @Inject + public StrutsProxyCacheFactory( + @Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize, + @Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) { + super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType)); + } +} +``` + +### 3. `core/src/main/java/org/apache/struts2/util/ProxyService.java` + +Service interface with proxy detection methods. + +```java +package org.apache.struts2.util; + +import java.lang.reflect.Member; + +/** + * Service interface for proxy detection and resolution operations. + * Replaces static ProxyUtil methods with an injectable service. + * + * @since 7.2.0 + */ +public interface ProxyService { + + /** + * Determine the ultimate target class of the given instance. + */ + Class<?> ultimateTargetClass(Object candidate); + + /** + * Check whether the given object is a proxy. + */ + boolean isProxy(Object object); + + /** + * Check whether the given member is a proxy member. + */ + boolean isProxyMember(Member member, Object object); + + /** + * Check whether the given object is a Hibernate proxy. + */ + boolean isHibernateProxy(Object object); + + /** + * Check whether the given member is a member of a Hibernate proxy. + */ + boolean isHibernateProxyMember(Member member); + + /** + * Get the target instance of a Hibernate proxy. + */ + Object getHibernateProxyTarget(Object object); + + /** + * Resolve matching member on target class. + */ + Member resolveTargetMember(Member proxyMember, Class<?> targetClass); + + /** + * @deprecated since 7.2, use {@link #resolveTargetMember(Member, Class)} instead. + */ + @Deprecated + Member resolveTargetMember(Member proxyMember, Object target); +} +``` + +### 4. `core/src/main/java/org/apache/struts2/util/StrutsProxyService.java` + +Implementation using injected `ProxyCacheFactory`. Move logic from `ProxyUtil`. + +```java +package org.apache.struts2.util; + +import org.apache.struts2.inject.Inject; +import org.apache.struts2.ognl.OgnlCache; +import org.apache.struts2.ognl.ProxyCacheFactory; +// ... other imports from ProxyUtil + +/** + * Default implementation of {@link ProxyService}. + * + * @since 7.2.0 + */ +public class StrutsProxyService implements ProxyService { + + private final OgnlCache<Class<?>, Boolean> isProxyCache; + private final OgnlCache<Member, Boolean> isProxyMemberCache; + private final OgnlCache<Object, Class<?>> targetClassCache; + + @Inject + public StrutsProxyService(ProxyCacheFactory<?, ?> proxyCacheFactory) { + this.isProxyCache = proxyCacheFactory.buildOgnlCache(); + this.isProxyMemberCache = proxyCacheFactory.buildOgnlCache(); + this.targetClassCache = proxyCacheFactory.buildOgnlCache(); + } + + // ... implement all methods from ProxyService interface + // (move logic from ProxyUtil static methods) +} +``` + +### 5. `core/src/test/java/org/apache/struts2/ognl/StrutsProxyCacheFactoryTest.java` + +Unit tests for cache factory. + +### 6. `core/src/test/java/org/apache/struts2/util/StrutsProxyServiceTest.java` + +Unit tests for proxy service. + +--- + +## Files to Modify + +### 1. `core/src/main/java/org/apache/struts2/StrutsConstants.java` + +Add constants: + +```java +/** + * Specifies the type of cache to use for proxy detection. Valid values defined in + * {@link org.apache.struts2.ognl.OgnlCacheFactory.CacheType}. + * + * @since 7.2.0 + */ +public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType"; + +/** + * Specifies the maximum cache size for proxy detection caches. + * + * @since 7.2.0 + */ +public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize"; +``` + +### 2. `core/src/main/resources/org/apache/struts2/default.properties` + +Add defaults: + +```properties +### Proxy detection cache configuration +struts.proxy.cacheType=basic +struts.proxy.cacheMaxSize=10000 +``` + +### 3. `core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java` + +Add to `BOOTSTRAP_CONSTANTS` static block: + +```java +constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC); +constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000); +``` + +Add to `bootstrapFactories()` method: + +```java +.factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON) +.factory(ProxyService.class, StrutsProxyService.class, Scope.SINGLETON) +``` + +### 4. `core/src/main/resources/struts-beans.xml` + +Add bean registrations: + +```xml +<bean type="org.apache.struts2.ognl.ProxyCacheFactory" name="struts" + class="org.apache.struts2.ognl.StrutsProxyCacheFactory" scope="singleton"/> +<bean type="org.apache.struts2.util.ProxyService" name="struts" + class="org.apache.struts2.util.StrutsProxyService" scope="singleton"/> +``` + +### 5. `core/src/main/java/org/apache/struts2/util/ProxyUtil.java` + +Deprecate all public static methods: + +```java +/** + * @deprecated since 7.2, inject {@link ProxyService} instead + */ +@Deprecated(since = "7.2") +public static boolean isProxy(Object object) { + // existing implementation kept for backwards compatibility +} +``` + +### 6. `core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java` + +Add ProxyService injection and update calls: + +```java +private ProxyService proxyService; + +@Inject +public void setProxyService(ProxyService proxyService) { + this.proxyService = proxyService; +} +``` + +Replace: +- `ProxyUtil.isProxy()` → `proxyService.isProxy()` +- `ProxyUtil.isProxyMember()` → `proxyService.isProxyMember()` +- `ProxyUtil.ultimateTargetClass()` → `proxyService.ultimateTargetClass()` +- `ProxyUtil.resolveTargetMember()` → `proxyService.resolveTargetMember()` + +### 7. `core/src/main/java/org/apache/struts2/interceptor/parameter/ParametersInterceptor.java` + +Add ProxyService field and setter, update `ultimateClass()` method. + +### 8. `core/src/main/java/org/apache/struts2/interceptor/ChainingInterceptor.java` + +Add ProxyService field and setter, update proxy detection calls. + +### 9. `core/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java` + +Add ProxyService field and setter, update `ultimateTargetClass()` call. + +### 10. `plugins/spring/src/test/java/org/apache/struts2/spring/SpringProxyUtilTest.java` + +Update to test new `ProxyService` alongside deprecated `ProxyUtil`. + +--- + +## Implementation Order + +| Step | File | Action | +|------|------|--------| +| 1 | `StrutsConstants.java` | Add 2 constants | +| 2 | `ProxyCacheFactory.java` | Create marker interface | +| 3 | `StrutsProxyCacheFactory.java` | Create implementation | +| 4 | `ProxyService.java` | Create service interface | +| 5 | `StrutsProxyService.java` | Create implementation with injected factory | +| 6 | `DefaultConfiguration.java` | Register constants + factories | +| 7 | `struts-beans.xml` | Register beans | +| 8 | `default.properties` | Add default values | +| 9 | `ProxyUtil.java` | Add deprecation annotations | +| 10 | `SecurityMemberAccess.java` | Inject and use ProxyService | +| 11 | `ParametersInterceptor.java` | Inject and use ProxyService | +| 12 | `ChainingInterceptor.java` | Inject and use ProxyService | +| 13 | `DefaultJSONWriter.java` | Inject and use ProxyService | +| 14 | Tests | Create unit tests for factory and service | +| 15 | `SpringProxyUtilTest.java` | Update integration tests | + +--- + +## User Configuration + +After implementation, users can configure: + +```xml +<constant name="struts.proxy.cacheType" value="basic" /> +<constant name="struts.proxy.cacheMaxSize" value="5000" /> +``` + +Valid cache types: `basic`, `lru`, `wtlfu` + +--- + +## Verification + +1. **Build**: `mvn clean install -DskipTests` +2. **Unit Tests**: `mvn test -DskipAssembly -pl core -Dtest=StrutsProxyCacheFactoryTest,StrutsProxyServiceTest` +3. **Integration Tests**: `mvn test -DskipAssembly -pl plugins/spring -Dtest=SpringProxyUtilTest` +4. **Full Test Suite**: `mvn test -DskipAssembly` +5. **Verify no Caffeine required**: Configure `struts.proxy.cacheType=basic` and confirm app starts without Caffeine + +--- + +## Key Design Decisions + +| Decision | Rationale | +|----------|-----------| +| Default `BASIC` cache | Avoids mandatory Caffeine dependency (original issue) | +| Singleton scope | Caches should be shared application-wide | +| Keep deprecated `ProxyUtil` | Backwards compatibility for existing code | +| `ProxyService` in `util` package | Discoverability alongside `ProxyUtil` | +| `StrutsProxyCacheFactory` naming | Follows user preference and Struts conventions | + +--- + +## Code References + +- `core/src/main/java/org/apache/struts2/util/ProxyUtil.java:53-58` - Current hardcoded WTLFU caches +- `core/src/main/java/org/apache/struts2/ognl/DefaultOgnlExpressionCacheFactory.java` - Pattern to follow +- `core/src/main/java/org/apache/struts2/ognl/ExpressionCacheFactory.java` - Interface pattern +- `core/src/main/java/org/apache/struts2/ognl/SecurityMemberAccess.java:217-225` - Heaviest ProxyUtil consumer +- `core/src/main/resources/struts-beans.xml:239-242` - Bean registration pattern + +--- + +## Related Research + +- OgnlUtil injection pattern analysis +- ProxyUtil usage analysis across codebase diff --git a/thoughts/shared/validation/2026-02-08-WW-5514-validation.md b/thoughts/shared/validation/2026-02-08-WW-5514-validation.md new file mode 100644 index 000000000..261d6ace6 --- /dev/null +++ b/thoughts/shared/validation/2026-02-08-WW-5514-validation.md @@ -0,0 +1,175 @@ +# Validation Report: WW-5514 Proxy Cache Configuration + +**Date**: 2026-02-08 +**Research Document**: `thoughts/shared/research/2026-02-07-WW-5514-proxy-cache-configuration.md` +**Status**: ✅ **VALIDATED** + +## Executive Summary + +The WW-5514 proxy cache configuration implementation has been validated successfully. All build and test phases pass. +Two additional test files required fixes for `ProxyService` injection that were not identified in the original research +document. + +## Validation Results + +### Phase 1: Build Verification ✅ + +``` +mvn clean install -DskipTests +BUILD SUCCESS (01:10 min) +``` + +All 28 modules compiled successfully. + +### Phase 2: New Unit Tests ✅ + +``` +mvn test -DskipAssembly -pl core -Dtest=StrutsProxyCacheFactoryTest,StrutsProxyServiceTest +Tests run: 17, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +### Phase 3: Updated Core Tests ✅ + +``` +mvn test -DskipAssembly -pl core -Dtest=SecurityMemberAccessTest,StrutsParameterAnnotationTest +Tests run: 81, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +### Phase 4: Full Test Suite ✅ + +``` +mvn test -DskipAssembly +Tests run: ~1500+, Failures: 0, Errors: 0 +BUILD SUCCESS (01:04 min) +``` + +## Implementation Completeness + +### New Files Created (4/4) ✅ + +| File | Status | +|----------------------------------------------|-----------| +| `core/.../ognl/ProxyCacheFactory.java` | ✅ Created | +| `core/.../ognl/StrutsProxyCacheFactory.java` | ✅ Created | +| `core/.../util/ProxyService.java` | ✅ Created | +| `core/.../util/StrutsProxyService.java` | ✅ Created | + +### Files Modified (5/5) ✅ + +| File | Status | +|-----------------------------|----------------------------| +| `StrutsConstants.java` | ✅ 4 new constants | +| `default.properties` | ✅ 4 new properties | +| `DefaultConfiguration.java` | ✅ Bootstrap + factory | +| `struts-beans.xml` | ✅ Bean registrations | +| `ProxyUtil.java` | ✅ @Deprecated(since="7.2") | + +### Consumer Integration (4/4) ✅ + +| Class | ProxyService Integration | +|-------------------------|--------------------------| +| `SecurityMemberAccess` | ✅ | +| `ParametersInterceptor` | ✅ | +| `ChainingInterceptor` | ✅ | +| `DefaultJSONWriter` | ✅ | + +### Test Files (4 documented + 2 additional fixes) + +| File | Status | Notes | +|--------------------------------------|-------------|------------------------------| +| `StrutsProxyCacheFactoryTest.java` | ✅ NEW | 6 test methods | +| `StrutsProxyServiceTest.java` | ✅ NEW | 11 test methods | +| `SecurityMemberAccessTest.java` | ✅ UPDATED | ProxyService injected | +| `StrutsParameterAnnotationTest.java` | ✅ UPDATED | ProxyService injected | +| `SecurityMemberAccessProxyTest.java` | ✅ **FIXED** | ProxyService injection added | +| `JSONResultTest.java` | ✅ **FIXED** | ProxyService injection added | + +## Issues Found and Fixed + +### Issue 1: SecurityMemberAccessProxyTest (Spring Plugin) + +**Location**: `plugins/spring/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessProxyTest.java` + +**Problem**: The test created `SecurityMemberAccess` as a field with `new SecurityMemberAccess(null, null)` without +injecting `ProxyService`, causing NullPointerException. + +**Fix Applied**: + +```java +// Added imports +import org.apache.struts2.util.StrutsProxyService; +import org.apache.struts2.util.ProxyService; + +// Changed field declaration +private SecurityMemberAccess sma; +private ProxyService proxyService; + +// Added to setUp() +proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); +sma = new SecurityMemberAccess(null, null); +sma.setProxyService(proxyService); +``` + +### Issue 2: JSONResultTest (JSON Plugin) + +**Location**: `plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java` + +**Problem**: The test `testNotTraverseOrIncludeProxyInfo` created `DefaultJSONWriter` directly without injecting +`ProxyService`, causing NullPointerException when processing Spring proxies. + +**Fix Applied**: + +```java +// Added imports +import org.apache.struts2.ognl.StrutsProxyCacheFactory; +import org.apache.struts2.util.StrutsProxyService; +import org.apache.struts2.util.ProxyService; + +// Added field +ProxyService proxyService; + +// Added to setUp() +this.proxyService = new StrutsProxyService(new StrutsProxyCacheFactory<>("1000", "basic")); + +// Updated testNotTraverseOrIncludeProxyInfo() +DefaultJSONWriter writer = new DefaultJSONWriter(); +writer.setProxyService(proxyService); +``` + +## Configuration Properties Verified + +| Property | Default Value | Purpose | +|-----------------------------|---------------|---------------------------------------| +| `struts.proxy.cacheType` | `basic` | Cache implementation (basic/caffeine) | +| `struts.proxy.cacheMaxSize` | `10000` | Maximum cache entries | +| `struts.proxy.cacheFactory` | `struts` | Factory implementation name | +| `struts.proxyService` | `struts` | Service implementation name | + +## Success Criteria Status + +| Criterion | Status | +|----------------------------------------------------------|-------------------| +| Build passes: `mvn clean install -DskipTests` | ✅ PASS | +| Unit tests pass: New tests | ✅ PASS (17 tests) | +| Updated tests pass: Core tests | ✅ PASS (81 tests) | +| Full test suite: `mvn test -DskipAssembly` | ✅ PASS | +| No Caffeine required with `struts.proxy.cacheType=basic` | ✅ VERIFIED | + +## Minor Gap Identified + +**SpringProxyUtilTest.java** - Not updated to test `ProxyService` alongside deprecated `ProxyUtil` + +- **Impact**: Low - tests deprecated API which still works +- **Recommendation**: Can be addressed in follow-up if needed + +## Conclusion + +The WW-5514 implementation is **complete and validated**. Two test files in plugin modules required additional fixes for +`ProxyService` injection that were not identified in the original research document. All tests now pass successfully. + +### Files Modified During Validation + +1. `plugins/spring/src/test/java/org/apache/struts2/ognl/SecurityMemberAccessProxyTest.java` +2. `plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java`
