dongjoon-hyun commented on code in PR #58604:
URL: https://github.com/apache/spark/pull/58604#discussion_r3955464252
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -2676,6 +2676,18 @@ object SQLConf {
.booleanConf
.createWithDefault(true)
+ val RESTRICTED_MODE_ENABLED = buildConf("spark.sql.restrictedMode.enabled")
+ .internal()
+ .doc("When true, SQL features that load or execute externally provided
code, scripts, or " +
+ "jars from the query itself are disabled: the reflect and java_method
functions and the " +
+ "TRANSFORM ... USING clause are rejected. This is an opt-in profile for
endpoints that run " +
+ "queries from many users; it composes with the individual data-source
option controls " +
+ "(for example spark.sql.kafka.disallowedOptions and the avro schema-URL
scheme allowlist). " +
Review Comment:
`spark.sql.kafka.disallowedOptions` does not exist anywhere in the
repository -- this doc string is the only occurrence. I could not find an "avro
schema-URL scheme allowlist" either.
Could you drop these examples, or point at configs that actually exist?
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/RestrictedModeSuite.scala:
##########
@@ -0,0 +1,64 @@
+/*
+ * 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.catalyst.analysis
+
+import org.apache.spark.sql.AnalysisException
+import org.apache.spark.sql.catalyst.expressions.{AttributeReference,
CallMethodViaReflection, Literal}
+import org.apache.spark.sql.catalyst.plans.logical.{ScriptInputOutputSchema,
ScriptTransformation}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.StringType
+
+class RestrictedModeSuite extends AnalysisTest {
+
+ private val restricted = SQLConf.RESTRICTED_MODE_ENABLED.key
+
+ private def reflectCall: CallMethodViaReflection =
+ CallMethodViaReflection(Seq(Literal("java.lang.Math"), Literal("abs"),
Literal(-1)))
+
+ private def transformPlan: ScriptTransformation =
+ ScriptTransformation(
+ "cat",
+ Seq(AttributeReference("value", StringType)()),
+ TestRelations.testRelation,
+ ScriptInputOutputSchema(Nil, Nil, None, None, Nil, Nil, None, None,
schemaLess = false))
+
+ test("reflect/java_method are rejected only when restricted mode is
enabled") {
+ withSQLConf(restricted -> "true") {
+ val e = intercept[AnalysisException](reflectCall.checkInputDataTypes())
+ assert(e.getMessage.contains("restrictedMode.enabled"))
Review Comment:
A substring match on the message verifies neither the new error condition
name nor its parameters -- it would still pass if the error came from an
unrelated code path that happened to mention the config. `checkError` is the
convention here (82 uses in this test directory alone):
```scala
checkError(
exception = e,
condition = "UNSUPPORTED_FEATURE.SQL_RESTRICTED_MODE",
parameters = Map(
"feature" -> "...",
"config" -> "\"spark.sql.restrictedMode.enabled\""))
```
Same for the `TRANSFORM` assertion below.
Two cases also look worth adding: `try_reflect` (blocked via the
`TryReflect` replacement, but untested), and an end-to-end SQL test for `SELECT
TRANSFORM ... USING` -- the current test builds the `ScriptTransformation` node
directly, so it would not catch a plan shape that reaches execution without
passing through this `CheckAnalysis` traversal.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/CheckAnalysis.scala:
##########
@@ -375,6 +375,10 @@ trait CheckAnalysis extends LookupCatalog with
QueryErrorsBase with PlanToString
plan.foreachUp {
case p if p.analyzed => // Skip already analyzed sub-plans
+ case _: ScriptTransformation if SQLConf.get.restrictedModeEnabled =>
Review Comment:
This case sits below `case p if p.analyzed => // Skip already analyzed
sub-plans`, and `AnalysisHelper.setAnalyzed()` marks children recursively:
```scala
private[sql] def setAnalyzed(): Unit = {
...
children.foreach(_.setAnalyzed())
}
```
So a `ScriptTransformation` that lives inside an already-analyzed sub-plan
never reaches this case. That covers DataFrame-backed temp views
(`createOrReplaceTempView` stores the analyzed plan) and reused `Dataset`s, so
a view created while the mode was off can still be queried after the mode is
turned on.
The `reflect` gate has the analogous problem: `checkInputDataTypes()` is
reached through the `Expression.resolved` lazy val, so the conf value is
memoized at first resolution.
If the mode is meant to be a boundary rather than a hint, the check probably
needs to run somewhere that pre-analyzed plans cannot skip. At minimum it is
worth calling out the limitation.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/CallMethodViaReflection.scala:
##########
@@ -84,6 +84,10 @@ case class CallMethodViaReflection(
override def prettyName: String =
getTagValue(FunctionRegistry.FUNC_ALIAS).getOrElse("reflect")
override def checkInputDataTypes(): TypeCheckResult = {
+ if (SQLConf.get.restrictedModeEnabled) {
+ throw QueryCompilationErrors.restrictedModeFeatureError(
+ "The reflect and java_method functions")
Review Comment:
Two issues with this string.
**Grammar.** Substituted into `"<feature> is not allowed because <config> is
enabled."` and prefixed by the `UNSUPPORTED_FEATURE` parent message, users see:
> The feature is not supported: The reflect and java_method function**s**
**is** not allowed because "spark.sql.restrictedMode.enabled" is enabled.
**`try_reflect` is missing.** `TryReflect` replaces itself with
`CallMethodViaReflection(params, failOnError = false)`, so `try_reflect` is
blocked by this check too, but the message never mentions it.
Using `prettyName` fixes both at once, since `prettyName` already resolves
to `reflect` / `java_method` / `try_reflect` via `FUNC_ALIAS`:
```scala
throw QueryCompilationErrors.restrictedModeFeatureError(
s"The ${toSQLId(prettyName)} function")
```
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -2676,6 +2676,18 @@ object SQLConf {
.booleanConf
.createWithDefault(true)
+ val RESTRICTED_MODE_ENABLED = buildConf("spark.sql.restrictedMode.enabled")
Review Comment:
This should very likely be `buildStaticConf` rather than `buildConf`.
The doc below describes this as "an opt-in profile for endpoints that run
queries from many users", but as a runtime `SQLConf` any of those users can
disable the whole thing with
```sql
SET spark.sql.restrictedMode.enabled=false;
```
`internal()` only hides the config from the docs and `SET -v`; it does not
make it read-only. Spark already uses `buildStaticConf` for exactly this kind
of setting -- see
`spark.sql.legacy.hive.thriftServer.allowSettingSystemProperties` and
`spark.sql.hive.thriftServer.http.sniHostCheckEnabled` in
`StaticSQLConf.scala`. With a static conf, `RuntimeConfig.requireNonStaticConf`
rejects the `SET`.
As written, the gate can be turned off by the very users it is meant to
constrain, which undercuts the rest of the PR.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryCompilationErrors.scala:
##########
@@ -329,6 +329,14 @@ private[sql] object QueryCompilationErrors extends
QueryErrorsBase with Compilat
messageParameters = Map.empty)
}
+ def restrictedModeFeatureError(feature: String): Throwable = {
+ new AnalysisException(
+ errorClass = "UNSUPPORTED_FEATURE.SQL_RESTRICTED_MODE",
+ messageParameters = Map(
+ "feature" -> feature,
+ "config" -> toSQLConfVal(SQLConf.RESTRICTED_MODE_ENABLED.key)))
Review Comment:
This is a config *key*, so it should be `toSQLConf`, not `toSQLConfVal`. The
convention is spelled out in `QueryErrorsBase`'s own header comment:
> 5. SQL configs ... For example: "spark.sql.ansi.enabled".
> 6. Any values of datasource options or SQL configs shall be double quoted.
For example: "true", "CORRECTED".
`Count.scala` shows the correct pairing:
```scala
toSQLConf(SQLConf.ALLOW_PARAMETERLESS_COUNT.key),
toSQLConfVal(true.toString))
```
Both helpers happen to emit the same double quotes today, so this is not a
behavior change -- but the two names mean opposite things.
Separately, on the error condition itself: `<feature>` taking free-form
English prose is the only such parameter in `error-conditions.json`. Splitting
into per-feature sub-conditions would match how the rest of the file is written.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]