This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new e081f47b5848 [SPARK-57509][CORE] Return `FORBIDDEN` instead of 
`METHOD_NOT_ALLOWED` for disallowed `/workers/kill` requests
e081f47b5848 is described below

commit e081f47b5848cfb72a8a901639f2e273af1ccffc
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Wed Jun 17 14:27:08 2026 -0700

    [SPARK-57509][CORE] Return `FORBIDDEN` instead of `METHOD_NOT_ALLOWED` for 
disallowed `/workers/kill` requests
    
    ### What changes were proposed in this pull request?
    
    When the Master Web UI `/workers/kill` endpoint rejects a decommission 
request, return `403 FORBIDDEN` instead of `405 METHOD_NOT_ALLOWED`.
    
    ```scala
     if (!isDecommissioningRequestAllowed(req)) {
    -  resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
    +  resp.sendError(HttpServletResponse.SC_FORBIDDEN)
     }
    ```
    
    This is a subtask of SPARK-55556 `Improve Web Security`.
    
    ### Why are the changes needed?
    
    The original feature was introduced at Apache Spark 3.1.0.
    - https://github.com/apache/spark/pull/29015
    
    The request is rejected by policy/origin 
(`spark.master.ui.decommission.allow.mode` is `DENY`, or `LOCAL` with a 
non-local caller), not because of the HTTP method. `405` means the method is 
unsupported and must carry an `Allow` header; `403` ("understood the request 
but refuses to authorize it") is the correct code, consistent with 
`HttpSecurityFilter` and `JWSFilter` in the same module.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes but only a different HTTP ErrorCode is returned because a disallowed 
`/workers/kill` request now returns `403` instead of `405`. The functional 
behavior is the same rejection. So, no functional behavior change.
    
    ### How was this patch tested?
    
    Pass the CIs with the newly added test case.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.8)
    
    Closes #56573 from dongjoon-hyun/SPARK-57509.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
    (cherry picked from commit ec83fe65458b072d14cc6ef9be1c22e9e2e3d5cf)
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../spark/deploy/master/ui/MasterWebUI.scala       |  2 +-
 .../spark/deploy/master/ui/MasterWebUISuite.scala  | 22 ++++++++++++++++++++++
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git 
a/core/src/main/scala/org/apache/spark/deploy/master/ui/MasterWebUI.scala 
b/core/src/main/scala/org/apache/spark/deploy/master/ui/MasterWebUI.scala
index 66036e7a5e5c..3e047c98f872 100644
--- a/core/src/main/scala/org/apache/spark/deploy/master/ui/MasterWebUI.scala
+++ b/core/src/main/scala/org/apache/spark/deploy/master/ui/MasterWebUI.scala
@@ -75,7 +75,7 @@ class MasterWebUI(
           val hostnames: Seq[String] = Option(req.getParameterValues("host"))
             .getOrElse(Array[String]()).toImmutableArraySeq
           if (!isDecommissioningRequestAllowed(req)) {
-            resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED)
+            resp.sendError(HttpServletResponse.SC_FORBIDDEN)
           } else {
             val removedWorkers = masterEndpointRef.askSync[Integer](
               DecommissionWorkersOnHosts(hostnames))
diff --git 
a/core/src/test/scala/org/apache/spark/deploy/master/ui/MasterWebUISuite.scala 
b/core/src/test/scala/org/apache/spark/deploy/master/ui/MasterWebUISuite.scala
index 5e75d1c424ea..37d0c5ec692d 100644
--- 
a/core/src/test/scala/org/apache/spark/deploy/master/ui/MasterWebUISuite.scala
+++ 
b/core/src/test/scala/org/apache/spark/deploy/master/ui/MasterWebUISuite.scala
@@ -24,6 +24,7 @@ import java.util.Date
 
 import scala.collection.mutable.HashMap
 
+import jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN
 import org.mockito.Mockito.{mock, times, verify, when}
 
 import org.apache.spark.{SecurityManager, SparkConf, SparkFunSuite}
@@ -31,6 +32,7 @@ import 
org.apache.spark.deploy.DeployMessages.{DecommissionWorkersOnHosts, KillD
 import org.apache.spark.deploy.DeployTestUtils._
 import org.apache.spark.deploy.master._
 import org.apache.spark.internal.config.DECOMMISSION_ENABLED
+import org.apache.spark.internal.config.UI.MASTER_UI_DECOMMISSION_ALLOW_MODE
 import org.apache.spark.rpc.{RpcEndpointRef, RpcEnv}
 import org.apache.spark.util.Utils
 
@@ -106,6 +108,26 @@ class MasterWebUISuite extends SparkFunSuite {
   test("Kill multiple hosts") {
     testKillWorkers(Seq("noSuchHost", "LocalHost"))
   }
+
+  test("SPARK-57509: /workers/kill responds with 403 Forbidden when the 
request is not allowed") {
+    val denyConf = new SparkConf()
+      .set(DECOMMISSION_ENABLED, true)
+      .set(MASTER_UI_DECOMMISSION_ALLOW_MODE.key, "DENY")
+    val denyMaster = mock(classOf[Master])
+    when(denyMaster.securityMgr).thenReturn(new SecurityManager(denyConf))
+    when(denyMaster.conf).thenReturn(denyConf)
+    when(denyMaster.rpcEnv).thenReturn(rpcEnv)
+    when(denyMaster.self).thenReturn(masterEndpointRef)
+    val denyWebUI = new MasterWebUI(denyMaster, 0)
+    try {
+      denyWebUI.bind()
+      val url = 
s"http://${Utils.localHostNameForURI()}:${denyWebUI.boundPort}/workers/kill/"
+      val body = convPostDataToString(Seq(("host", 
Utils.localHostNameForURI())))
+      assert(sendHttpRequest(url, "POST", body).getResponseCode === 
SC_FORBIDDEN)
+    } finally {
+      denyWebUI.stop()
+    }
+  }
 }
 
 object MasterWebUISuite {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to