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");
+    }
+}

Reply via email to