This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 5c991141b WW-5718 fix(rest,bean-validation): add the
resource-isolation interceptors to the plugin default stacks (#1957)
5c991141b is described below
commit 5c991141b697cfb3449c65eac5bf3b126d4d5189
Author: Lukasz Lenart <[email protected]>
AuthorDate: Wed Sep 16 19:32:05 2026 +0200
WW-5718 fix(rest,bean-validation): add the resource-isolation interceptors
to the plugin default stacks (#1957)
* WW-5718 fix(rest,bean-validation): add the resource-isolation
interceptors to the plugin default stacks
restDefaultStack and beanValidationDefaultStack were forked from core's
defaultStack before WW-5083 added coep, coop and fetchMetadata in 6.0.0
and were never brought back in step, so a package extending rest-default
or struts-bean-validation silently lost the COOP/COEP headers and the
Fetch Metadata request check. The three refs are copied with
defaultStack's parameters into the same slot: after conversionError,
ahead of validation.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
* WW-5718 fix(rest): answer an HTTP-status result code with that status
FetchMetadataInterceptor rejects a cross-site request by returning "403"
as the result code without invoking the action. RestActionInvocation
wrapped any unknown code in a DefaultHttpHeaders whose status defaults
to 200, so a JSON or XML client saw a 200 - carrying the unexecuted
model for a GET. A bare HTTP status returned as the result code now sets
the response status and drops the representation; a mapped "403" result
still executes as before.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../src/main/resources/struts-plugin.xml | 13 +++++
.../BeanValidationDefaultStackTest.java | 60 ++++++++++++++++++++++
.../apache/struts2/rest/RestActionInvocation.java | 9 ++++
plugins/rest/src/main/resources/struts-plugin.xml | 13 +++++
.../struts2/rest/RestActionInvocationTest.java | 23 +++++++++
.../apache/struts2/rest/RestDefaultStackTest.java | 60 ++++++++++++++++++++++
6 files changed, 178 insertions(+)
diff --git a/plugins/bean-validation/src/main/resources/struts-plugin.xml
b/plugins/bean-validation/src/main/resources/struts-plugin.xml
index 530366af5..80b3111cd 100644
--- a/plugins/bean-validation/src/main/resources/struts-plugin.xml
+++ b/plugins/bean-validation/src/main/resources/struts-plugin.xml
@@ -55,6 +55,19 @@
<interceptor-ref name="actionMappingParams"/>
<interceptor-ref name="params"/>
<interceptor-ref name="conversionError"/>
+ <interceptor-ref name="coep">
+ <param name="disabled">false</param>
+ <param name="enforcingMode">false</param>
+ <param name="exemptedPaths"/>
+ </interceptor-ref>
+ <interceptor-ref name="coop">
+ <param name="disabled">false</param>
+ <param name="exemptedPaths"/>
+ <param name="mode">same-origin</param>
+ </interceptor-ref>
+ <interceptor-ref name="fetchMetadata">
+ <param name="disabled">false</param>
+ </interceptor-ref>
<interceptor-ref name="beanValidation">
<param
name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
diff --git
a/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/BeanValidationDefaultStackTest.java
b/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/BeanValidationDefaultStackTest.java
new file mode 100644
index 000000000..164f63048
--- /dev/null
+++
b/plugins/bean-validation/src/test/java/org/apache/struts/beanvalidation/BeanValidationDefaultStackTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.struts.beanvalidation;
+
+import org.apache.struts2.XWorkTestCase;
+import org.apache.struts2.config.DefaultPropertiesProvider;
+import org.apache.struts2.config.StrutsBeanSelectionProvider;
+import org.apache.struts2.config.StrutsXmlConfigurationProvider;
+import org.apache.struts2.config.entities.InterceptorMapping;
+import org.apache.struts2.config.entities.InterceptorStackConfig;
+import org.apache.struts2.config.entities.PackageConfig;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * WW-5718: {@code beanValidationDefaultStack} was forked from core's {@code
defaultStack} and has to carry
+ * the same resource-isolation interceptors, in the same slot - after {@code
conversionError} and
+ * ahead of validation - or a package extending {@code struts-bean-validation}
silently loses them.
+ */
+public class BeanValidationDefaultStackTest extends XWorkTestCase {
+
+ private List<String> stack;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ loadConfigurationProviders(
+ new StrutsXmlConfigurationProvider("struts-default.xml"),
+ new StrutsXmlConfigurationProvider("struts-plugin.xml"),
+ new DefaultPropertiesProvider(),
+ new StrutsBeanSelectionProvider());
+ PackageConfig beanValidationDefault =
configuration.getPackageConfig("struts-bean-validation");
+ assertEquals("beanValidationDefaultStack",
beanValidationDefault.getDefaultInterceptorRef());
+ InterceptorStackConfig stackConfig =
+ (InterceptorStackConfig)
beanValidationDefault.getInterceptorConfig("beanValidationDefaultStack");
+ stack =
stackConfig.getInterceptors().stream().map(InterceptorMapping::getName).toList();
+ }
+
+ public void
testResourceIsolationInterceptorsSitBetweenConversionErrorAndValidation() {
+ assertThat(stack).containsSubsequence("conversionError", "coep",
"coop", "fetchMetadata", "beanValidation");
+ }
+}
diff --git
a/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
b/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
index 2defb5773..3fbcc98ef 100644
---
a/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
+++
b/plugins/rest/src/main/java/org/apache/struts2/rest/RestActionInvocation.java
@@ -174,6 +174,11 @@ public class RestActionInvocation extends
DefaultActionInvocation {
// Get the httpHeaders
if (httpHeaders == null) {
httpHeaders = new DefaultHttpHeaders(resultCode);
+ if (isHttpStatus(resultCode)) {
+ // an interceptor short-circuited with a status, not a
representation
+ httpHeaders.setStatus(Integer.parseInt(resultCode));
+ target = null;
+ }
}
// Apply headers
@@ -228,6 +233,10 @@ public class RestActionInvocation extends
DefaultActionInvocation {
}
}
+ private static boolean isHttpStatus(String resultCode) {
+ return resultCode != null && resultCode.matches("[1-5]\\d\\d");
+ }
+
/**
* Get the status code from HttpHeaderResult and it is saved in the
HttpHeaders object.
*/
diff --git a/plugins/rest/src/main/resources/struts-plugin.xml
b/plugins/rest/src/main/resources/struts-plugin.xml
index c6bb08e0e..c2116856e 100644
--- a/plugins/rest/src/main/resources/struts-plugin.xml
+++ b/plugins/rest/src/main/resources/struts-plugin.xml
@@ -94,6 +94,19 @@
<interceptor-ref name="params"/>
<interceptor-ref name="rest" />
<interceptor-ref name="conversionError"/>
+ <interceptor-ref name="coep">
+ <param name="disabled">false</param>
+ <param name="enforcingMode">false</param>
+ <param name="exemptedPaths"/>
+ </interceptor-ref>
+ <interceptor-ref name="coop">
+ <param name="disabled">false</param>
+ <param name="exemptedPaths"/>
+ <param name="mode">same-origin</param>
+ </interceptor-ref>
+ <interceptor-ref name="fetchMetadata">
+ <param name="disabled">false</param>
+ </interceptor-ref>
<interceptor-ref name="validation">
<param
name="excludeMethods">input,back,cancel,browse,index,show,edit,editNew</param>
</interceptor-ref>
diff --git
a/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
b/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
index f45932c5e..a1adf5158 100644
---
a/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
+++
b/plugins/rest/src/test/java/org/apache/struts2/rest/RestActionInvocationTest.java
@@ -37,6 +37,7 @@ import junit.framework.TestCase;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.ognl.StrutsOgnlGuard;
import org.apache.struts2.result.HttpHeaderResult;
+import org.apache.struts2.rest.handler.JacksonJsonHandler;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
@@ -46,7 +47,9 @@ import java.util.List;
import java.util.Map;
import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.BASIC;
+import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+import static org.assertj.core.api.Assertions.assertThat;
public class RestActionInvocationTest extends TestCase {
@@ -209,6 +212,26 @@ public class RestActionInvocationTest extends TestCase {
}
+ /**
+ * WW-5718: an interceptor that short-circuits with an HTTP status as the
result code
+ * ({@code FetchMetadataInterceptor} returns {@code "403"}) must not be
answered with 200
+ * and the unexecuted model once the REST stack carries it.
+ */
+ public void
testHttpStatusResultCodeWithoutMappedResultIsAnsweredWithThatStatus() throws
Exception {
+ DefaultContentTypeHandlerManager handlerManager = new
DefaultContentTypeHandlerManager();
+ handlerManager.handlersByExtension.put("json", new
JacksonJsonHandler());
+ restActionInvocation.setMimeTypeHandlerSelector(handlerManager);
+ request.setMethod("GET");
+ request.setRequestURI("/dogs.json");
+ ((RestAction) restActionInvocation.getAction()).model =
List.of("Item");
+ restActionInvocation.setResultCode("403");
+
+ restActionInvocation.processResult();
+
+ assertEquals(SC_FORBIDDEN, response.getStatus());
+ assertThat(response.getContentAsString()).doesNotContain("Item");
+ }
+
public void testNoResult() throws Exception {
RestAction restAction = (RestAction)restActionInvocation.getAction();
diff --git
a/plugins/rest/src/test/java/org/apache/struts2/rest/RestDefaultStackTest.java
b/plugins/rest/src/test/java/org/apache/struts2/rest/RestDefaultStackTest.java
new file mode 100644
index 000000000..125edd919
--- /dev/null
+++
b/plugins/rest/src/test/java/org/apache/struts2/rest/RestDefaultStackTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.rest;
+
+import org.apache.struts2.XWorkTestCase;
+import org.apache.struts2.config.DefaultPropertiesProvider;
+import org.apache.struts2.config.StrutsBeanSelectionProvider;
+import org.apache.struts2.config.StrutsXmlConfigurationProvider;
+import org.apache.struts2.config.entities.InterceptorMapping;
+import org.apache.struts2.config.entities.InterceptorStackConfig;
+import org.apache.struts2.config.entities.PackageConfig;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * WW-5718: {@code restDefaultStack} was forked from core's {@code
defaultStack} and has to carry
+ * the same resource-isolation interceptors, in the same slot - after {@code
conversionError} and
+ * ahead of validation - or a package extending {@code rest-default} silently
loses them.
+ */
+public class RestDefaultStackTest extends XWorkTestCase {
+
+ private List<String> stack;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ loadConfigurationProviders(
+ new StrutsXmlConfigurationProvider("struts-default.xml"),
+ new StrutsXmlConfigurationProvider("struts-plugin.xml"),
+ new DefaultPropertiesProvider(),
+ new StrutsBeanSelectionProvider());
+ PackageConfig restDefault =
configuration.getPackageConfig("rest-default");
+ assertEquals("restDefaultStack",
restDefault.getDefaultInterceptorRef());
+ InterceptorStackConfig stackConfig =
+ (InterceptorStackConfig)
restDefault.getInterceptorConfig("restDefaultStack");
+ stack =
stackConfig.getInterceptors().stream().map(InterceptorMapping::getName).toList();
+ }
+
+ public void
testResourceIsolationInterceptorsSitBetweenConversionErrorAndValidation() {
+ assertThat(stack).containsSubsequence("conversionError", "coep",
"coop", "fetchMetadata", "validation");
+ }
+}