Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,14 @@ package object config {
.stringConf
.createWithDefault("(?i)secret|password")

private[spark] val STRING_REDACTION_PATTERN =
ConfigBuilder("spark.redaction.string.regex")
.doc("Regex to decide which parts of strings produced by Spark contain sensitive " +
"information. When this regex matches a string part, that string part is replaced by a " +
"dummy value. This is currently used to redact the output of SQL explain commands.")
.stringConf
.createOptional

private[spark] val NETWORK_AUTH_ENABLED =
ConfigBuilder("spark.authenticate")
.booleanConf
Expand Down
16 changes: 15 additions & 1 deletion core/src/main/scala/org/apache/spark/util/Utils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2585,13 +2585,27 @@ private[spark] object Utils extends Logging {
}
}

private[util] val REDACTION_REPLACEMENT_TEXT = "*********(redacted)"
private[spark] val REDACTION_REPLACEMENT_TEXT = "*********(redacted)"

/**
* Redact the sensitive values in the given map. If a map key matches the redaction pattern then
* its value is replaced with a dummy text.
*/
def redact(conf: SparkConf, kvs: Seq[(String, String)]): Seq[(String, String)] = {
val redactionPattern = conf.get(SECRET_REDACTION_PATTERN).r
redact(redactionPattern, kvs)
}

/**
* Redact the sensitive information in the given string.
*/
def redact(conf: SparkConf, text: String): String = {
val redactionValue = conf.get(STRING_REDACTION_PATTERN)
if (text == null || text.isEmpty || redactionValue.isEmpty) return text
val redactionPattern = redactionValue.get.r
redactionPattern.replaceAllIn(text, REDACTION_REPLACEMENT_TEXT)
}

private def redact(redactionPattern: Regex, kvs: Seq[(String, String)]): Seq[(String, String)] = {
kvs.map { kv =>
redactionPattern.findFirstIn(kv._1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,33 @@ trait DataSourceScanExec extends LeafExecNode with CodegenSupport {
val relation: BaseRelation
val metastoreTableIdentifier: Option[TableIdentifier]

protected val nodeNamePrefix: String = ""

override val nodeName: String = {
s"Scan $relation ${metastoreTableIdentifier.map(_.unquotedString).getOrElse("")}"
}

override def simpleString: String = {
val metadataEntries = metadata.toSeq.sorted.map {
case (key, value) =>
key + ": " + StringUtils.abbreviate(redact(value), 100)
}
val metadataStr = Utils.truncatedString(metadataEntries, " ", ", ", "")
s"$nodeNamePrefix$nodeName${Utils.truncatedString(output, "[", ",", "]")}$metadataStr"
}

override def verboseString: String = redact(super.verboseString)

override def treeString(verbose: Boolean, addSuffix: Boolean): String = {
redact(super.treeString(verbose, addSuffix))
}

/**
* Shorthand for calling redactString() without specifying redacting rules
* */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: * */ -> */

private def redact(text: String): String = {
Utils.redact(SparkSession.getActiveSession.get.sparkContext.conf, text)
}
}

/** Physical plan node for scanning data from a relation. */
Expand Down Expand Up @@ -85,15 +109,6 @@ case class RowDataSourceScanExec(
}
}

override def simpleString: String = {
val metadataEntries = for ((key, value) <- metadata.toSeq.sorted) yield {
key + ": " + StringUtils.abbreviate(value, 100)
}

s"$nodeName${Utils.truncatedString(output, "[", ",", "]")}" +
s"${Utils.truncatedString(metadataEntries, " ", ", ", "")}"
}

override def inputRDDs(): Seq[RDD[InternalRow]] = {
rdd :: Nil
}
Expand Down Expand Up @@ -307,13 +322,7 @@ case class FileSourceScanExec(
}
}

override def simpleString: String = {
val metadataEntries = for ((key, value) <- metadata.toSeq.sorted) yield {
key + ": " + StringUtils.abbreviate(value, 100)
}
val metadataStr = Utils.truncatedString(metadataEntries, " ", ", ", "")
s"File$nodeName${Utils.truncatedString(output, "[", ",", "]")}$metadataStr"
}
override val nodeNamePrefix: String = "File"

override protected def doProduce(ctx: CodegenContext): String = {
if (supportsBatch) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.spark.sql.execution

import org.apache.hadoop.fs.Path

import org.apache.spark.sql.QueryTest
import org.apache.spark.sql.test.SharedSQLContext
import org.apache.spark.util.Utils

/**
* Suite that tests the redaction of DataSourceScanExec
*/
class DataSourceScanExecRedactionSuite extends QueryTest with SharedSQLContext {

import Utils._

override def beforeAll(): Unit = {
sparkConf.set("spark.redaction.string.regex",
"spark-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
super.beforeAll()
}

test("treeString is redacted") {
withTempDir { dir =>
val basePath = dir.getCanonicalPath
spark.range(0, 10).toDF("a").write.parquet(new Path(basePath, "foo=1").toString)
val df = spark.read.parquet(basePath)

val rootPath = df.queryExecution.sparkPlan.find(_.isInstanceOf[FileSourceScanExec]).get
.asInstanceOf[FileSourceScanExec].relation.location.rootPaths.head
assert(rootPath.toString.contains(basePath.toString))

assert(!df.queryExecution.sparkPlan.treeString(verbose = true).contains(rootPath.getName))
assert(!df.queryExecution.executedPlan.treeString(verbose = true).contains(rootPath.getName))
assert(!df.queryExecution.toString.contains(rootPath.getName))
assert(!df.queryExecution.simpleString.contains(rootPath.getName))

val replacement = "*********"
assert(df.queryExecution.sparkPlan.treeString(verbose = true).contains(replacement))
assert(df.queryExecution.executedPlan.treeString(verbose = true).contains(replacement))
assert(df.queryExecution.toString.contains(replacement))
assert(df.queryExecution.simpleString.contains(replacement))
}
}
}