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

cloud-fan pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new f21fdf4d4172 [SPARK-59118][4.X][SQL] Warn when doAs is enabled but not 
enforced
f21fdf4d4172 is described below

commit f21fdf4d41727b170f5e1af7f5385f427fdbfa52
Author: Holden Karau <[email protected]>
AuthorDate: Tue Oct 27 14:04:15 2026 +0000

    [SPARK-59118][4.X][SQL] Warn when doAs is enabled but not enforced
    
    ### What changes were proposed in this pull request?
    
    This is a softer follow on of https://github.com/apache/spark/pull/58414 
for 4.X branch which warns when doAs is enabled but not enforced. We should log 
rather than fail a previously running job.
    
    ### Why are the changes needed?
    
    People might expect doAs to run as a different user, which is not supported.
    
    ### Does this PR introduce _any_ user-facing change?
    
    The Spark Thrift Server logs a startup warning when 
`hive.server2.enable.doAs` is enabled with an authenticating auth type, unless 
the new static conf `spark.sql.hive.thriftServer.allowIneffectiveDoAs` is set 
to `true`.
    
    ### How was this patch tested?
    
    New unit test.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Yes, claude
    
    Closes #58438 from holdenk/SPARK-59118-doAs-warn-4.x-r2.
    
    Lead-authored-by: Holden Karau <[email protected]>
    Co-authored-by: Holden Karau <[email protected]>
    Co-authored-by: Wenchen Fan <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 docs/sql-migration-guide.md                        |   1 +
 .../apache/spark/sql/internal/StaticSQLConf.scala  |  17 ++++
 .../sql/hive/thriftserver/HiveThriftServer2.scala  |  47 ++++++++++
 .../thriftserver/HiveThriftServer2DoAsSuite.scala  | 104 +++++++++++++++++++++
 .../SparkExecuteStatementOperationSuite.scala      |  24 +++++
 5 files changed, 193 insertions(+)

diff --git a/docs/sql-migration-guide.md b/docs/sql-migration-guide.md
index d228ba14050b..647151f9ba57 100644
--- a/docs/sql-migration-guide.md
+++ b/docs/sql-migration-guide.md
@@ -57,6 +57,7 @@ license: |
 - Since Spark 4.4, with whole-stage codegen enabled, a filter condition, 
including the `FILTER` clause of an aggregate function, evaluates the remaining 
top-level conjuncts of the condition on a row after one of them evaluates to 
`NULL`, as the SQL standard requires, and does so in the order they appear in 
the condition, as Spark already did with whole-stage codegen disabled, which 
also evaluates a subexpression shared by several conjuncts before the 
condition. Only a `FALSE` conjunct st [...]
 
 - Since Spark 4.4, when a format is specified, `to_timestamp`, 
`try_to_timestamp`, and `to_timestamp_ltz` cast a numeric first argument to a 
string and parse it with the format, for example `to_timestamp(20201231, 
'yyyyMMdd')` returns `2020-12-31 00:00:00`. This matches the existing behavior 
when `spark.sql.timestampType` is `TIMESTAMP_NTZ`. Previously, with the default 
`TIMESTAMP_LTZ` timestamp type, such calls failed with `INTERNAL_ERROR`. Note 
that the string form of the number is par [...]
+- Since Spark 4.4, the Spark Thrift Server logs a warning at startup when 
`hive.server2.enable.doAs` is `true` and `hive.server2.authentication` verifies 
user identities (any recognized type other than `NONE`/`NOSASL`), because 
executor-side data access still runs as the server's own service identity 
rather than the connecting user (SPARK-5159). Set 
`spark.sql.hive.thriftServer.allowIneffectiveDoAs` to `true` to acknowledge the 
limitation and silence the warning. Spark 5.0 is expected to [...]
 
 ## Upgrading from Spark SQL 4.2 to 4.3
 
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala
index d00036b131d7..24b5647ca187 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/StaticSQLConf.scala
@@ -147,6 +147,23 @@ object StaticSQLConf {
       .booleanConf
       .createWithDefault(true)
 
+  val HIVE_THRIFT_SERVER_ALLOW_INEFFECTIVE_DOAS =
+    buildStaticConf("spark.sql.hive.thriftServer.allowIneffectiveDoAs")
+      .doc("With hive.server2.enable.doAs=true the Spark Thrift Server 
impersonates the " +
+        "connecting user on the driver, but executor-side data access still 
runs as the " +
+        "server's own service identity (SPARK-5159), so storage-level 
permissions are " +
+        "checked against the privileged service principal instead of the 
impersonated " +
+        "user. Because that can silently grant users access to data they could 
not read " +
+        "with their own credentials, the server warns at startup when " +
+        "hive.server2.enable.doAs=true and hive.server2.authentication 
verifies user " +
+        "identities (any recognized type other than NONE/NOSASL), unless this 
option is " +
+        "set to true to acknowledge the limitation. Spark 5.0 is expected to 
refuse to " +
+        "start on that configuration unless this option is set.")
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)
+      .booleanConf
+      .createWithDefault(false)
+
   val SPARK_SESSION_EXTENSIONS = buildStaticConf("spark.sql.extensions")
     .doc("A comma-separated list of classes that implement " +
       "Function1[SparkSessionExtensions, Unit] used to configure Spark Session 
extensions. The " +
diff --git 
a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2.scala
 
b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2.scala
index 3825c6ed750f..ded58feda956 100644
--- 
a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2.scala
+++ 
b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2.scala
@@ -23,12 +23,15 @@ import java.util.concurrent.atomic.AtomicBoolean
 import org.apache.hadoop.hive.common.ServerUtils
 import org.apache.hadoop.hive.conf.HiveConf
 import org.apache.hadoop.hive.conf.HiveConf.ConfVars
+import org.apache.hive.service.auth.HiveAuthFactory.AuthTypes
 import org.apache.hive.service.cli.thrift.{ThriftBinaryCLIService, 
ThriftHttpCLIService}
 import org.apache.hive.service.server.HiveServer2
 
 import org.apache.spark.SparkContext
 import org.apache.spark.annotation.{DeveloperApi, Since}
+import org.apache.spark.internal.LogEntry
 import org.apache.spark.internal.Logging
+import org.apache.spark.internal.LogKeys.{CONFIG, CONFIG2}
 import org.apache.spark.internal.config.UI.UI_ENABLED
 import org.apache.spark.sql.{SparkSession, SQLContext}
 import org.apache.spark.sql.hive.HiveUtils
@@ -105,6 +108,48 @@ object HiveThriftServer2 extends Logging {
     }
   }
 
+  // Sessions are impersonated on the driver (metastore calls, driver-side 
file system access),
+  // but executor-side data access runs as the service identity (SPARK-5159), 
so storage ACLs
+  // are checked against the wrong principal. NONE/NOSASL are exempt: they 
establish no verified
+  // identity (the client picks any username it likes, and doAs then 
impersonates that
+  // unverified name for metastore calls), so there is no security boundary to 
warn about --
+  // and Hive's out-of-the-box config (auth NONE, doAs true) must stay quiet. 
Unrecognized auth
+  // types are left alone too: HiveAuthFactory rejects them with its own 
"Unsupported
+  // authentication type" error, which points at the actual problem.
+  private[thriftserver] def ineffectiveDoAsWarning(
+      hiveConf: HiveConf,
+      allowIneffectiveDoAs: Boolean): Option[LogEntry] = {
+    val authType = hiveConf.getVar(ConfVars.HIVE_SERVER2_AUTHENTICATION)
+    val verifyingAuthTypes =
+      AuthTypes.values().filterNot(Set(AuthTypes.NONE, 
AuthTypes.NOSASL)).map(_.getAuthName)
+    // getVar returns the default (NONE) when unset and "" when set empty, 
never null.
+    val authVerifiesUser = 
verifyingAuthTypes.exists(_.equalsIgnoreCase(authType))
+    if (authVerifiesUser && 
hiveConf.getBoolVar(ConfVars.HIVE_SERVER2_ENABLE_DOAS) &&
+        !allowIneffectiveDoAs) {
+      Some(log"${MDC(CONFIG, ConfVars.HIVE_SERVER2_ENABLE_DOAS.varname)} is 
set to true, " +
+        log"but the Spark Thrift Server impersonates the connecting user only 
on the driver, " +
+        log"for Hive metastore calls and driver-side file system access: 
executor-side data " +
+        log"access still runs as the server's own service identity, so storage 
permissions " +
+        log"are checked against the service principal rather than the 
connecting user " +
+        log"(SPARK-5159). That can expose data the connecting user is not 
authorized to " +
+        log"read. Setting it to false silences this, but note that it stops 
impersonating " +
+        log"metastore calls too, so it is not a no-op (and unsetting it does 
not help -- " +
+        log"Hive's own default is true). To acknowledge the limitation and 
silence this " +
+        log"instead, set " +
+        log"${MDC(CONFIG2, 
StaticSQLConf.HIVE_THRIFT_SERVER_ALLOW_INEFFECTIVE_DOAS.key)} to " +
+        log"true; it is a static conf, so it must be set before the 
SparkSession is created. " +
+        log"Spark 5.0 is expected to refuse to start on this configuration 
unless it is set.")
+    } else {
+      None
+    }
+  }
+
+  private[thriftserver] def warnIfIneffectiveDoAs(
+      hiveConf: HiveConf,
+      allowIneffectiveDoAs: Boolean): Unit = {
+    ineffectiveDoAsWarning(hiveConf, 
allowIneffectiveDoAs).foreach(logWarning(_))
+  }
+
   def main(args: Array[String]): Unit = {
     // If the arguments contains "-h" or "--help", print out the usage and 
exit.
     if (args.contains("-h") || args.contains("--help")) {
@@ -154,6 +199,8 @@ private[hive] class HiveThriftServer2(sparkSession: 
SparkSession)
   private val started = new AtomicBoolean(false)
 
   override def init(hiveConf: HiveConf): Unit = {
+    HiveThriftServer2.warnIfIneffectiveDoAs(
+      hiveConf, 
sparkSession.conf.get(StaticSQLConf.HIVE_THRIFT_SERVER_ALLOW_INEFFECTIVE_DOAS))
     val sparkSqlCliService = new SparkSQLCLIService(this, sparkSession)
     setSuperField(this, "cliService", sparkSqlCliService)
     addService(sparkSqlCliService)
diff --git 
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2DoAsSuite.scala
 
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2DoAsSuite.scala
new file mode 100644
index 000000000000..5fd89731f4a6
--- /dev/null
+++ 
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/HiveThriftServer2DoAsSuite.scala
@@ -0,0 +1,104 @@
+/*
+ * 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.spark.sql.hive.thriftserver
+
+import org.apache.hadoop.hive.conf.HiveConf
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars
+import org.apache.hive.service.auth.HiveAuthFactory.AuthTypes
+
+import org.apache.spark.SparkFunSuite
+import org.apache.spark.sql.internal.StaticSQLConf
+
+/**
+ * Tests for the SPARK-59118 startup warning: `hive.server2.enable.doAs` is 
accepted but
+ * impersonation does not reach executor-side data access. Spark 5.0 is 
expected to refuse
+ * to start instead, on the same configuration and with the same opt-out conf.
+ */
+class HiveThriftServer2DoAsSuite extends SparkFunSuite {
+
+  private def hiveConf(authType: String, doAs: Boolean): HiveConf = {
+    val conf = new HiveConf()
+    conf.setVar(ConfVars.HIVE_SERVER2_AUTHENTICATION, authType)
+    conf.setBoolVar(ConfVars.HIVE_SERVER2_ENABLE_DOAS, doAs)
+    conf
+  }
+
+  /**
+   * The warning text (if any) that checking `conf` would log. These tests 
assert on the
+   * returned entry rather than captured log output, which is hostage to 
JVM-global logging
+   * state when the whole module shares one forked JVM (SERIAL_SBT_TESTS=1 in 
CI).
+   */
+  private def doAsWarnings(conf: HiveConf, allowIneffectiveDoAs: Boolean = 
false): Seq[String] = {
+    HiveThriftServer2.ineffectiveDoAsWarning(conf, 
allowIneffectiveDoAs).map(_.message).toSeq
+  }
+
+  // The auth types that establish a user identity worth impersonating.
+  private val verifyingAuthTypes =
+    AuthTypes.values().filterNot(Set(AuthTypes.NONE, 
AuthTypes.NOSASL)).map(_.getAuthName)
+
+  test("SPARK-5159 / SPARK-59118 warn when doAs is enabled but not enforced") {
+    val warnings = doAsWarnings(hiveConf("KERBEROS", doAs = true))
+    assert(warnings.length == 1)
+    assert(warnings.head.contains("SPARK-5159"))
+    // Auth type matching is case-insensitive.
+    assert(doAsWarnings(hiveConf("kerberos", doAs = true)).length == 1)
+  }
+
+  test("SPARK-59118 allowIneffectiveDoAs=true silences an otherwise-warned 
config") {
+    assert(doAsWarnings(hiveConf("KERBEROS", doAs = true), 
allowIneffectiveDoAs = true).isEmpty)
+  }
+
+  test("SPARK-59118 the warning names the conf that silences it") {
+    val warning = doAsWarnings(hiveConf("KERBEROS", doAs = true)).head
+    
assert(warning.contains(StaticSQLConf.HIVE_THRIFT_SERVER_ALLOW_INEFFECTIVE_DOAS.key))
+  }
+
+  test("SPARK-59118 warn for every auth type that verifies the user") {
+    // Guards against a new AuthTypes value silently landing on the quiet side.
+    assert(verifyingAuthTypes.nonEmpty)
+    verifyingAuthTypes.foreach { authType =>
+      assert(doAsWarnings(hiveConf(authType, doAs = true)).length == 1, s"no 
warning: $authType")
+    }
+  }
+
+  test("SPARK-59118 does not warn when auth type establishes no user 
identity") {
+    Seq("NONE", "NOSASL", "none", "noSasl").foreach { authType =>
+      assert(doAsWarnings(hiveConf(authType, doAs = true)).isEmpty, s"warned: 
$authType")
+    }
+  }
+
+  test("SPARK-59118 does not warn when doAs is disabled") {
+    verifyingAuthTypes.foreach { authType =>
+      assert(doAsWarnings(hiveConf(authType, doAs = false)).isEmpty, s"warned: 
$authType")
+    }
+  }
+
+  test("SPARK-59118 does not warn on an untouched Hive config") {
+    // Hive defaults doAs to true and auth to NONE, so a stock config must 
stay quiet.
+    assert(doAsWarnings(new HiveConf()).isEmpty)
+  }
+
+  test("SPARK-59118 an unrecognized or empty auth type is left to Hive's own 
error") {
+    // HiveAuthFactory rejects these with "Unsupported authentication type", 
where the
+    // warning's advice would not help. getVar returns "" (never null) for an 
explicitly
+    // empty value, so this must not NPE.
+    Seq("NOT_AN_AUTH_TYPE", "").foreach { authType =>
+      assert(doAsWarnings(hiveConf(authType, doAs = true)).isEmpty, s"warned: 
$authType")
+    }
+  }
+}
diff --git 
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperationSuite.scala
 
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperationSuite.scala
index ddd7f19ef238..6e62395cedc4 100644
--- 
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperationSuite.scala
+++ 
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/SparkExecuteStatementOperationSuite.scala
@@ -23,9 +23,11 @@ import java.util.concurrent.Semaphore
 import scala.concurrent.duration._
 
 import org.apache.hadoop.hive.conf.HiveConf
+import org.apache.hadoop.hive.conf.HiveConf.ConfVars
 import org.apache.hive.service.cli.OperationState
 import org.apache.hive.service.cli.session.{HiveSession, HiveSessionImpl}
 import org.apache.hive.service.rpc.thrift.{TProtocolVersion, TTypeId}
+import org.apache.logging.log4j.Level
 import org.mockito.Mockito.{doReturn, mock, spy, when, RETURNS_DEEP_STUBS}
 import org.mockito.invocation.InvocationOnMock
 
@@ -135,6 +137,28 @@ class SparkExecuteStatementOperationSuite extends 
SharedSparkSession {
     }
   }
 
+  test("SPARK-59118 HiveThriftServer2.init warns when doAs is not enforced") {
+    // The warning is only worth anything if init() actually calls it.
+    val hiveConf = new HiveConf()
+    hiveConf.setVar(ConfVars.HIVE_SERVER2_AUTHENTICATION, "KERBEROS")
+    hiveConf.setBoolVar(ConfVars.HIVE_SERVER2_ENABLE_DOAS, true)
+    // init() boots Hive's service graph; operation logging attaches an 
appender to the root
+    // logger that nothing ever removes, so keep it off rather than leak it 
into this JVM.
+    hiveConf.setBoolVar(ConfVars.HIVE_SERVER2_LOGGING_OPERATION_ENABLED, false)
+    // Attach to the root logger, not the HiveThriftServer2 logger by name: 
withLogAppender on
+    // a named logger leaves behind a non-additive LoggerConfig (log4j2 copies 
root's additivity)
+    // that swallows that logger's output for every later suite in this JVM.
+    val appender = new LogAppender("doAs impersonation warning")
+    withLogAppender(appender, level = Some(Level.WARN)) {
+      new HiveThriftServer2(spark).init(hiveConf)
+    }
+    val warnings = appender.loggingEvents.map(_.getMessage.getFormattedMessage)
+    assert(warnings.exists { warning =>
+      warning.contains(ConfVars.HIVE_SERVER2_ENABLE_DOAS.varname) &&
+        warning.contains("SPARK-5159")
+    })
+  }
+
   private class MySparkExecuteStatementOperation(
       session: SparkSession,
       hiveSession: HiveSession,


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

Reply via email to