From c5a1b50d712f7a213a412cf27fae4089f54ba5c7 Mon Sep 17 00:00:00 2001 From: "Han, Yutong" Date: Tue, 7 Apr 2026 21:07:05 -0700 Subject: [PATCH 1/6] session API security improvements --- .../apache/kyuubi/server/api/ApiUtils.scala | 14 ++++++--- .../server/api/v1/SessionsResource.scala | 10 +++++-- .../server/api/v1/SessionsResourceSuite.scala | 29 +++++++++++++++++++ .../server/rest/client/SessionCtlSuite.scala | 4 +-- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala index 1676ba15759..655147f8001 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala @@ -22,13 +22,16 @@ import scala.collection.JavaConverters._ import org.apache.kyuubi.{Logging, Utils} import org.apache.kyuubi.client.api.v1.dto import org.apache.kyuubi.client.api.v1.dto.{OperationData, OperationProgress, ServerData, SessionData} +import org.apache.kyuubi.config.KyuubiConf.SERVER_SECRET_REDACTION_PATTERN import org.apache.kyuubi.ha.client.ServiceNodeInfo import org.apache.kyuubi.operation.KyuubiOperation import org.apache.kyuubi.session.KyuubiSession object ApiUtils extends Logging { def sessionEvent(session: KyuubiSession): dto.KyuubiSessionEvent = { - session.getSessionEvent.map(event => + session.getSessionEvent.map { event => + val redactionPattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) + val redactedConf = Utils.redact(redactionPattern, event.conf.toSeq).toMap.asJava dto.KyuubiSessionEvent.builder() .sessionId(event.sessionId) .clientVersion(event.clientVersion) @@ -37,7 +40,7 @@ object ApiUtils extends Logging { .user(event.user) .clientIp(event.clientIP) .serverIp(event.serverIP) - .conf(event.conf.asJava) + .conf(redactedConf) .remoteSessionId(event.remoteSessionId) .engineId(event.engineId) .engineName(event.engineName) @@ -48,17 +51,20 @@ object ApiUtils extends Logging { .endTime(event.endTime) .totalOperations(event.totalOperations) .exception(event.exception.orNull) - .build()).orNull + .build() + }.orNull } def sessionData(session: KyuubiSession): SessionData = { val sessionEvent = session.getSessionEvent + val redactionPattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) + val redactedConf = Utils.redact(redactionPattern, session.conf.toSeq).toMap.asJava new SessionData( session.handle.identifier.toString, sessionEvent.map(_.remoteSessionId).getOrElse(""), session.user, session.ipAddress, - session.conf.asJava, + redactedConf, session.createTime, session.lastAccessTime - session.createTime, session.getNoOperationTime, diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala index 2113067ccd7..d165599f05f 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala @@ -52,11 +52,15 @@ private[v1] class SessionsResource extends ApiRequestContext with Logging { content = Array(new Content( mediaType = MediaType.APPLICATION_JSON, array = new ArraySchema(schema = new Schema(implementation = classOf[SessionData])))), - description = "get the list of all live sessions") + description = "get the list of live sessions for the current user") @GET def sessions(): Seq[SessionData] = { - sessionManager.allSessions() - .map(session => ApiUtils.sessionData(session.asInstanceOf[KyuubiSession])).toSeq + val userName = fe.getSessionUser(Map.empty[String, String]) + sessionManager + .allSessions() + .filter(session => session.user == userName) + .map(session => ApiUtils.sessionData(session.asInstanceOf[KyuubiSession])) + .toSeq } @ApiResponse( diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala index af49598fe82..13624b0f563 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala @@ -40,6 +40,12 @@ import org.apache.kyuubi.session.SessionType class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { + override protected lazy val conf: KyuubiConf = { + val c = KyuubiConf() + c.set(KyuubiConf.SERVER_SECRET_REDACTION_PATTERN, "(?i)password".r) + c + } + override protected def beforeEach(): Unit = { super.beforeEach() eventually(timeout(10.seconds), interval(200.milliseconds)) { @@ -389,4 +395,27 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { assert(operations.size == 1) assert(sessionHandle.toString.equals(operations.head.getSessionId)) } + + test("get /sessions returns redacted spark confs") { + val sensitiveKey = "spark.password" + val sensitiveValue = "superSecret123" + val requestObj = new SessionOpenRequest(Map(sensitiveKey -> sensitiveValue).asJava) + + val response = webTarget.path("api/v1/sessions") + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) + assert(200 == response.getStatus) + val sessionHandle = response.readEntity(classOf[SessionHandle]).getIdentifier + + val response2 = webTarget.path("api/v1/sessions").request().get() + assert(200 == response2.getStatus) + val sessions = response2.readEntity(new GenericType[Seq[SessionData]]() {}) + val sessionConf = sessions.find(_.getIdentifier == sessionHandle.toString).get.getConf + + assert(sessionConf.get(sensitiveKey) != sensitiveValue) + assert(sessionConf.get(sensitiveKey) == "*********(redacted)") + + val delResp = webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() + assert(200 == delResp.getStatus) + } } diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionCtlSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionCtlSuite.scala index fb43fcf8169..1338c8e5d9a 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionCtlSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/rest/client/SessionCtlSuite.scala @@ -38,13 +38,13 @@ class SessionCtlSuite extends RestClientTestHelper with TestPrematureExit { test("list sessions") { fe.be.sessionManager.openSession( TProtocolVersion.findByValue(1), - "admin", + clientPrincipalUser, "123456", "localhost", Map("testConfig" -> "testValue")) val args = Array("list", "session", "--authSchema", "spnego") - testPrematureExitForControlCli(args, "Session List (total 1)") + testPrematureExitForControlCli(args, "Live Session List (total 1)") } } From 1b3e81e82b6300331aa6303ed5a1684bd3e517ce Mon Sep 17 00:00:00 2001 From: "Han, Yutong" Date: Sun, 12 Apr 2026 22:18:11 -0700 Subject: [PATCH 2/6] add kyuubi.session.conf.display.mode config (REDACTED/ORIGINAL/NONE) --- .../org/apache/kyuubi/config/KyuubiConf.scala | 15 ++++++ .../apache/kyuubi/server/api/ApiUtils.scala | 25 +++++++--- .../server/api/v1/SessionsResourceSuite.scala | 48 ++++++++++++++++++- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala index 8e8d31550e8..9d389b0f360 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala @@ -3336,6 +3336,21 @@ object KyuubiConf { .regexConf .createOptional + val SESSION_CONF_DISPLAY_MODE: ConfigEntry[String] = + buildConf("kyuubi.session.conf.display.mode") + .serverOnly + .doc("Controls how session configurations are returned in REST API responses. " + + "Supported values: " + + "") + .version("1.12.0") + .stringConf + .checkValues(Set("REDACTED", "ORIGINAL", "NONE")) + .createWithDefault("REDACTED") + val SERVER_PERIODIC_GC_INTERVAL: ConfigEntry[Long] = buildConf("kyuubi.server.periodicGC.interval") .doc("How often to trigger the periodic garbage collection. 0 will disable it.") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala index 655147f8001..4f5478376ba 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala @@ -22,16 +22,28 @@ import scala.collection.JavaConverters._ import org.apache.kyuubi.{Logging, Utils} import org.apache.kyuubi.client.api.v1.dto import org.apache.kyuubi.client.api.v1.dto.{OperationData, OperationProgress, ServerData, SessionData} -import org.apache.kyuubi.config.KyuubiConf.SERVER_SECRET_REDACTION_PATTERN +import org.apache.kyuubi.config.KyuubiConf.{SERVER_SECRET_REDACTION_PATTERN, SESSION_CONF_DISPLAY_MODE} import org.apache.kyuubi.ha.client.ServiceNodeInfo import org.apache.kyuubi.operation.KyuubiOperation import org.apache.kyuubi.session.KyuubiSession object ApiUtils extends Logging { + + private def buildConf( + rawConf: Map[String, String], + session: KyuubiSession): java.util.Map[String, String] = { + session.sessionManager.getConf.get(SESSION_CONF_DISPLAY_MODE) match { + case "NONE" => Map.empty[String, String].asJava + case "ORIGINAL" => rawConf.asJava + case _ => + val pattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) + Utils.redact(pattern, rawConf.toSeq).toMap.asJava + } + } + def sessionEvent(session: KyuubiSession): dto.KyuubiSessionEvent = { session.getSessionEvent.map { event => - val redactionPattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) - val redactedConf = Utils.redact(redactionPattern, event.conf.toSeq).toMap.asJava + val conf = buildConf(event.conf, session) dto.KyuubiSessionEvent.builder() .sessionId(event.sessionId) .clientVersion(event.clientVersion) @@ -40,7 +52,7 @@ object ApiUtils extends Logging { .user(event.user) .clientIp(event.clientIP) .serverIp(event.serverIP) - .conf(redactedConf) + .conf(conf) .remoteSessionId(event.remoteSessionId) .engineId(event.engineId) .engineName(event.engineName) @@ -57,14 +69,13 @@ object ApiUtils extends Logging { def sessionData(session: KyuubiSession): SessionData = { val sessionEvent = session.getSessionEvent - val redactionPattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) - val redactedConf = Utils.redact(redactionPattern, session.conf.toSeq).toMap.asJava + val conf = buildConf(session.conf, session) new SessionData( session.handle.identifier.toString, sessionEvent.map(_.remoteSessionId).getOrElse(""), session.user, session.ipAddress, - redactedConf, + conf, session.createTime, session.lastAccessTime - session.createTime, session.getNoOperationTime, diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala index 13624b0f563..d194f603a65 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala @@ -396,7 +396,7 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { assert(sessionHandle.toString.equals(operations.head.getSessionId)) } - test("get /sessions returns redacted spark confs") { + test("get /sessions returns redacted spark confs when mode is REDACTED") { val sensitiveKey = "spark.password" val sensitiveValue = "superSecret123" val requestObj = new SessionOpenRequest(Map(sensitiveKey -> sensitiveValue).asJava) @@ -418,4 +418,50 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { val delResp = webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() assert(200 == delResp.getStatus) } + + test("get /sessions returns empty conf when mode is NONE") { + withSessionConfDisplayMode("NONE") { + val requestObj = + new SessionOpenRequest(Map("spark.password" -> "secret", "key" -> "val").asJava) + val r = webTarget.path("api/v1/sessions") + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) + assert(200 == r.getStatus) + val sessionHandle = r.readEntity(classOf[SessionHandle]).getIdentifier + + val r2 = webTarget.path("api/v1/sessions").request().get() + assert(200 == r2.getStatus) + val sessions = r2.readEntity(new GenericType[Seq[SessionData]]() {}) + val sessionConf = sessions.find(_.getIdentifier == sessionHandle.toString).get.getConf + assert(sessionConf.isEmpty) + + webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() + } + } + + test("get /sessions returns raw conf when mode is ORIGINAL") { + withSessionConfDisplayMode("ORIGINAL") { + val sensitiveKey = "spark.password" + val sensitiveValue = "plainVisible" + val requestObj = new SessionOpenRequest(Map(sensitiveKey -> sensitiveValue).asJava) + val r = webTarget.path("api/v1/sessions") + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) + assert(200 == r.getStatus) + val sessionHandle = r.readEntity(classOf[SessionHandle]).getIdentifier + + val r2 = webTarget.path("api/v1/sessions").request().get() + assert(200 == r2.getStatus) + val sessions = r2.readEntity(new GenericType[Seq[SessionData]]() {}) + val sessionConf = sessions.find(_.getIdentifier == sessionHandle.toString).get.getConf + assert(sessionConf.get(sensitiveKey) == sensitiveValue) + + webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() + } + } + + private def withSessionConfDisplayMode(mode: String)(f: => Unit): Unit = { + conf.set(KyuubiConf.SESSION_CONF_DISPLAY_MODE, mode) + try f finally conf.set(KyuubiConf.SESSION_CONF_DISPLAY_MODE, "REDACTED") + } } From 52f90324ce3c7b0e109575ed2433cfe17553be24 Mon Sep 17 00:00:00 2001 From: "Han, Yutong" Date: Sun, 12 Apr 2026 22:19:06 -0700 Subject: [PATCH 3/6] Revert "add kyuubi.session.conf.display.mode config (REDACTED/ORIGINAL/NONE)" This reverts commit 1b3e81e82b6300331aa6303ed5a1684bd3e517ce. --- .../org/apache/kyuubi/config/KyuubiConf.scala | 15 ------ .../apache/kyuubi/server/api/ApiUtils.scala | 25 +++------- .../server/api/v1/SessionsResourceSuite.scala | 48 +------------------ 3 files changed, 8 insertions(+), 80 deletions(-) diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala index 9d389b0f360..8e8d31550e8 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala @@ -3336,21 +3336,6 @@ object KyuubiConf { .regexConf .createOptional - val SESSION_CONF_DISPLAY_MODE: ConfigEntry[String] = - buildConf("kyuubi.session.conf.display.mode") - .serverOnly - .doc("Controls how session configurations are returned in REST API responses. " + - "Supported values: " + - "
    " + - "
  • REDACTED: Mask values that match kyuubi.server.redaction.regex (default).
  • " + - "
  • ORIGINAL: Return the raw config values as-is.
  • " + - "
  • NONE: Omit the conf map from responses entirely.
  • " + - "
") - .version("1.12.0") - .stringConf - .checkValues(Set("REDACTED", "ORIGINAL", "NONE")) - .createWithDefault("REDACTED") - val SERVER_PERIODIC_GC_INTERVAL: ConfigEntry[Long] = buildConf("kyuubi.server.periodicGC.interval") .doc("How often to trigger the periodic garbage collection. 0 will disable it.") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala index 4f5478376ba..655147f8001 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala @@ -22,28 +22,16 @@ import scala.collection.JavaConverters._ import org.apache.kyuubi.{Logging, Utils} import org.apache.kyuubi.client.api.v1.dto import org.apache.kyuubi.client.api.v1.dto.{OperationData, OperationProgress, ServerData, SessionData} -import org.apache.kyuubi.config.KyuubiConf.{SERVER_SECRET_REDACTION_PATTERN, SESSION_CONF_DISPLAY_MODE} +import org.apache.kyuubi.config.KyuubiConf.SERVER_SECRET_REDACTION_PATTERN import org.apache.kyuubi.ha.client.ServiceNodeInfo import org.apache.kyuubi.operation.KyuubiOperation import org.apache.kyuubi.session.KyuubiSession object ApiUtils extends Logging { - - private def buildConf( - rawConf: Map[String, String], - session: KyuubiSession): java.util.Map[String, String] = { - session.sessionManager.getConf.get(SESSION_CONF_DISPLAY_MODE) match { - case "NONE" => Map.empty[String, String].asJava - case "ORIGINAL" => rawConf.asJava - case _ => - val pattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) - Utils.redact(pattern, rawConf.toSeq).toMap.asJava - } - } - def sessionEvent(session: KyuubiSession): dto.KyuubiSessionEvent = { session.getSessionEvent.map { event => - val conf = buildConf(event.conf, session) + val redactionPattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) + val redactedConf = Utils.redact(redactionPattern, event.conf.toSeq).toMap.asJava dto.KyuubiSessionEvent.builder() .sessionId(event.sessionId) .clientVersion(event.clientVersion) @@ -52,7 +40,7 @@ object ApiUtils extends Logging { .user(event.user) .clientIp(event.clientIP) .serverIp(event.serverIP) - .conf(conf) + .conf(redactedConf) .remoteSessionId(event.remoteSessionId) .engineId(event.engineId) .engineName(event.engineName) @@ -69,13 +57,14 @@ object ApiUtils extends Logging { def sessionData(session: KyuubiSession): SessionData = { val sessionEvent = session.getSessionEvent - val conf = buildConf(session.conf, session) + val redactionPattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) + val redactedConf = Utils.redact(redactionPattern, session.conf.toSeq).toMap.asJava new SessionData( session.handle.identifier.toString, sessionEvent.map(_.remoteSessionId).getOrElse(""), session.user, session.ipAddress, - conf, + redactedConf, session.createTime, session.lastAccessTime - session.createTime, session.getNoOperationTime, diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala index d194f603a65..13624b0f563 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala @@ -396,7 +396,7 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { assert(sessionHandle.toString.equals(operations.head.getSessionId)) } - test("get /sessions returns redacted spark confs when mode is REDACTED") { + test("get /sessions returns redacted spark confs") { val sensitiveKey = "spark.password" val sensitiveValue = "superSecret123" val requestObj = new SessionOpenRequest(Map(sensitiveKey -> sensitiveValue).asJava) @@ -418,50 +418,4 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { val delResp = webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() assert(200 == delResp.getStatus) } - - test("get /sessions returns empty conf when mode is NONE") { - withSessionConfDisplayMode("NONE") { - val requestObj = - new SessionOpenRequest(Map("spark.password" -> "secret", "key" -> "val").asJava) - val r = webTarget.path("api/v1/sessions") - .request(MediaType.APPLICATION_JSON_TYPE) - .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) - assert(200 == r.getStatus) - val sessionHandle = r.readEntity(classOf[SessionHandle]).getIdentifier - - val r2 = webTarget.path("api/v1/sessions").request().get() - assert(200 == r2.getStatus) - val sessions = r2.readEntity(new GenericType[Seq[SessionData]]() {}) - val sessionConf = sessions.find(_.getIdentifier == sessionHandle.toString).get.getConf - assert(sessionConf.isEmpty) - - webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() - } - } - - test("get /sessions returns raw conf when mode is ORIGINAL") { - withSessionConfDisplayMode("ORIGINAL") { - val sensitiveKey = "spark.password" - val sensitiveValue = "plainVisible" - val requestObj = new SessionOpenRequest(Map(sensitiveKey -> sensitiveValue).asJava) - val r = webTarget.path("api/v1/sessions") - .request(MediaType.APPLICATION_JSON_TYPE) - .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) - assert(200 == r.getStatus) - val sessionHandle = r.readEntity(classOf[SessionHandle]).getIdentifier - - val r2 = webTarget.path("api/v1/sessions").request().get() - assert(200 == r2.getStatus) - val sessions = r2.readEntity(new GenericType[Seq[SessionData]]() {}) - val sessionConf = sessions.find(_.getIdentifier == sessionHandle.toString).get.getConf - assert(sessionConf.get(sensitiveKey) == sensitiveValue) - - webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() - } - } - - private def withSessionConfDisplayMode(mode: String)(f: => Unit): Unit = { - conf.set(KyuubiConf.SESSION_CONF_DISPLAY_MODE, mode) - try f finally conf.set(KyuubiConf.SESSION_CONF_DISPLAY_MODE, "REDACTED") - } } From 14de1e1741311e0a50745230e70056d2abbd4f05 Mon Sep 17 00:00:00 2001 From: "Han, Yutong" Date: Sun, 12 Apr 2026 23:10:52 -0700 Subject: [PATCH 4/6] add server-side config with three modes to control session conf visibility in REST API and add tests cover three modes --- .../org/apache/kyuubi/config/KyuubiConf.scala | 15 ++++++ .../apache/kyuubi/server/api/ApiUtils.scala | 25 +++++++--- .../server/api/v1/SessionsResourceSuite.scala | 48 ++++++++++++++++++- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala index 8e8d31550e8..9d389b0f360 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala @@ -3336,6 +3336,21 @@ object KyuubiConf { .regexConf .createOptional + val SESSION_CONF_DISPLAY_MODE: ConfigEntry[String] = + buildConf("kyuubi.session.conf.display.mode") + .serverOnly + .doc("Controls how session configurations are returned in REST API responses. " + + "Supported values: " + + "
    " + + "
  • REDACTED: Mask values that match kyuubi.server.redaction.regex (default).
  • " + + "
  • ORIGINAL: Return the raw config values as-is.
  • " + + "
  • NONE: Omit the conf map from responses entirely.
  • " + + "
") + .version("1.12.0") + .stringConf + .checkValues(Set("REDACTED", "ORIGINAL", "NONE")) + .createWithDefault("REDACTED") + val SERVER_PERIODIC_GC_INTERVAL: ConfigEntry[Long] = buildConf("kyuubi.server.periodicGC.interval") .doc("How often to trigger the periodic garbage collection. 0 will disable it.") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala index 655147f8001..4f5478376ba 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala @@ -22,16 +22,28 @@ import scala.collection.JavaConverters._ import org.apache.kyuubi.{Logging, Utils} import org.apache.kyuubi.client.api.v1.dto import org.apache.kyuubi.client.api.v1.dto.{OperationData, OperationProgress, ServerData, SessionData} -import org.apache.kyuubi.config.KyuubiConf.SERVER_SECRET_REDACTION_PATTERN +import org.apache.kyuubi.config.KyuubiConf.{SERVER_SECRET_REDACTION_PATTERN, SESSION_CONF_DISPLAY_MODE} import org.apache.kyuubi.ha.client.ServiceNodeInfo import org.apache.kyuubi.operation.KyuubiOperation import org.apache.kyuubi.session.KyuubiSession object ApiUtils extends Logging { + + private def buildConf( + rawConf: Map[String, String], + session: KyuubiSession): java.util.Map[String, String] = { + session.sessionManager.getConf.get(SESSION_CONF_DISPLAY_MODE) match { + case "NONE" => Map.empty[String, String].asJava + case "ORIGINAL" => rawConf.asJava + case _ => + val pattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) + Utils.redact(pattern, rawConf.toSeq).toMap.asJava + } + } + def sessionEvent(session: KyuubiSession): dto.KyuubiSessionEvent = { session.getSessionEvent.map { event => - val redactionPattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) - val redactedConf = Utils.redact(redactionPattern, event.conf.toSeq).toMap.asJava + val conf = buildConf(event.conf, session) dto.KyuubiSessionEvent.builder() .sessionId(event.sessionId) .clientVersion(event.clientVersion) @@ -40,7 +52,7 @@ object ApiUtils extends Logging { .user(event.user) .clientIp(event.clientIP) .serverIp(event.serverIP) - .conf(redactedConf) + .conf(conf) .remoteSessionId(event.remoteSessionId) .engineId(event.engineId) .engineName(event.engineName) @@ -57,14 +69,13 @@ object ApiUtils extends Logging { def sessionData(session: KyuubiSession): SessionData = { val sessionEvent = session.getSessionEvent - val redactionPattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) - val redactedConf = Utils.redact(redactionPattern, session.conf.toSeq).toMap.asJava + val conf = buildConf(session.conf, session) new SessionData( session.handle.identifier.toString, sessionEvent.map(_.remoteSessionId).getOrElse(""), session.user, session.ipAddress, - redactedConf, + conf, session.createTime, session.lastAccessTime - session.createTime, session.getNoOperationTime, diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala index 13624b0f563..d194f603a65 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala @@ -396,7 +396,7 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { assert(sessionHandle.toString.equals(operations.head.getSessionId)) } - test("get /sessions returns redacted spark confs") { + test("get /sessions returns redacted spark confs when mode is REDACTED") { val sensitiveKey = "spark.password" val sensitiveValue = "superSecret123" val requestObj = new SessionOpenRequest(Map(sensitiveKey -> sensitiveValue).asJava) @@ -418,4 +418,50 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { val delResp = webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() assert(200 == delResp.getStatus) } + + test("get /sessions returns empty conf when mode is NONE") { + withSessionConfDisplayMode("NONE") { + val requestObj = + new SessionOpenRequest(Map("spark.password" -> "secret", "key" -> "val").asJava) + val r = webTarget.path("api/v1/sessions") + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) + assert(200 == r.getStatus) + val sessionHandle = r.readEntity(classOf[SessionHandle]).getIdentifier + + val r2 = webTarget.path("api/v1/sessions").request().get() + assert(200 == r2.getStatus) + val sessions = r2.readEntity(new GenericType[Seq[SessionData]]() {}) + val sessionConf = sessions.find(_.getIdentifier == sessionHandle.toString).get.getConf + assert(sessionConf.isEmpty) + + webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() + } + } + + test("get /sessions returns raw conf when mode is ORIGINAL") { + withSessionConfDisplayMode("ORIGINAL") { + val sensitiveKey = "spark.password" + val sensitiveValue = "plainVisible" + val requestObj = new SessionOpenRequest(Map(sensitiveKey -> sensitiveValue).asJava) + val r = webTarget.path("api/v1/sessions") + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(requestObj, MediaType.APPLICATION_JSON_TYPE)) + assert(200 == r.getStatus) + val sessionHandle = r.readEntity(classOf[SessionHandle]).getIdentifier + + val r2 = webTarget.path("api/v1/sessions").request().get() + assert(200 == r2.getStatus) + val sessions = r2.readEntity(new GenericType[Seq[SessionData]]() {}) + val sessionConf = sessions.find(_.getIdentifier == sessionHandle.toString).get.getConf + assert(sessionConf.get(sensitiveKey) == sensitiveValue) + + webTarget.path(s"api/v1/sessions/$sessionHandle").request().delete() + } + } + + private def withSessionConfDisplayMode(mode: String)(f: => Unit): Unit = { + conf.set(KyuubiConf.SESSION_CONF_DISPLAY_MODE, mode) + try f finally conf.set(KyuubiConf.SESSION_CONF_DISPLAY_MODE, "REDACTED") + } } From d06883257beab8e2dd867f90b5e91227b99752d0 Mon Sep 17 00:00:00 2001 From: "Han, Yutong" Date: Mon, 13 Apr 2026 18:54:20 -0700 Subject: [PATCH 5/6] Refactor --- docs/configuration/settings.md | 42 ++++++++++--------- docs/deployment/migration-guide.md | 1 + .../org/apache/kyuubi/config/KyuubiConf.scala | 15 ++++++- .../apache/kyuubi/server/api/ApiUtils.scala | 15 ++++--- .../server/api/v1/SessionsResource.scala | 16 ++++--- .../server/api/v1/SessionsResourceSuite.scala | 4 +- 6 files changed, 58 insertions(+), 35 deletions(-) diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md index 591860243ad..156bbc619a7 100644 --- a/docs/configuration/settings.md +++ b/docs/configuration/settings.md @@ -262,6 +262,7 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co | kyuubi.frontend.rest.bind.host | <undefined> | Hostname or IP of the machine on which to run the REST frontend service. | string | 1.4.0 | | kyuubi.frontend.rest.bind.port | 10099 | Port of the machine on which to run the REST frontend service. | int | 1.4.0 | | kyuubi.frontend.rest.jetty.stopTimeout | PT5S | Stop timeout for Jetty server used by the RESTful frontend service. | duration | 1.8.1 | +| kyuubi.frontend.rest.legacy.v1.sessionsReturnAllUsers | false | When true, GET /api/v1/sessions returns all sessions on the server regardless of the calling user (legacy behavior). When false (default), only sessions owned by the authenticated user are returned. This flag is provided for backward compatibility and will be removed in a future release. | boolean | 1.12.0 | | kyuubi.frontend.rest.max.worker.threads | 999 | Maximum number of threads in the frontend worker thread pool for the rest frontend service | int | 1.6.2 | | kyuubi.frontend.rest.proxy.jetty.client.idleTimeout | PT30S | The idle timeout in milliseconds for Jetty server used by the RESTful frontend service. | duration | 1.10.0 | | kyuubi.frontend.rest.proxy.jetty.client.maxConnections | 32768 | The max number of connections per destination for Jetty server used by the RESTful frontend service. | int | 1.10.0 | @@ -446,26 +447,27 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co ### Server -| Key | Default | Meaning | Type | Since | -|----------------------------------------------------------|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|--------| -| kyuubi.server.administrators || Comma-separated list of Kyuubi service administrators. We use this config to grant admin permission to any service accounts when security mechanism is enabled. Note, when kyuubi.authentication is configured to NOSASL or NONE, everyone is treated as administrator. | set | 1.8.0 | -| kyuubi.server.info.provider | ENGINE | The server information provider name, some clients may rely on this information to check the server compatibilities and functionalities.
  • SERVER: Return Kyuubi server information.
  • ENGINE: Return Kyuubi engine information.
  • | string | 1.6.1 | -| kyuubi.server.limit.batch.connections.per.ipaddress | <undefined> | Maximum kyuubi server batch connections per ipaddress. Any user exceeding this limit will not be allowed to connect. | int | 1.7.0 | -| kyuubi.server.limit.batch.connections.per.user | <undefined> | Maximum kyuubi server batch connections per user. Any user exceeding this limit will not be allowed to connect. | int | 1.7.0 | -| kyuubi.server.limit.batch.connections.per.user.ipaddress | <undefined> | Maximum kyuubi server batch connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect. | int | 1.7.0 | -| kyuubi.server.limit.client.fetch.max.rows | <undefined> | Max rows limit for getting result row set operation. If the max rows specified by client-side is larger than the limit, request will fail directly. | int | 1.8.0 | -| kyuubi.server.limit.connections.ip.deny.list || The client ip in the deny list will be denied to connect to kyuubi server. | set | 1.9.1 | -| kyuubi.server.limit.connections.per.ipaddress | <undefined> | Maximum kyuubi server connections per ipaddress. Any user exceeding this limit will not be allowed to connect. | int | 1.6.0 | -| kyuubi.server.limit.connections.per.user | <undefined> | Maximum kyuubi server connections per user. Any user exceeding this limit will not be allowed to connect. | int | 1.6.0 | -| kyuubi.server.limit.connections.per.user.ipaddress | <undefined> | Maximum kyuubi server connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect. | int | 1.6.0 | -| kyuubi.server.limit.connections.user.deny.list || The user in the deny list will be denied to connect to kyuubi server, if the user has configured both user.unlimited.list and user.deny.list, the priority of the latter is higher. | set | 1.8.0 | -| kyuubi.server.limit.connections.user.unlimited.list || The maximum connections of the user in the white list will not be limited. | set | 1.7.0 | -| kyuubi.server.name | <undefined> | The name of Kyuubi Server. | string | 1.5.0 | -| kyuubi.server.periodicGC.interval | PT30M | How often to trigger the periodic garbage collection. 0 will disable it. | duration | 1.7.0 | -| kyuubi.server.redaction.regex | <undefined> | Regex to decide which Kyuubi contain sensitive information. When this regex matches a property key or value, the value is redacted from the various logs. || 1.6.0 | -| kyuubi.server.tempFile.expireTime | P30D | Expiration timout for cleanup server-side temporary files, e.g. operation logs. | duration | 1.10.0 | -| kyuubi.server.tempFile.maxCount | <undefined> | The upper threshold size of server-side temporary file paths to cleanup | int | 1.10.0 | -| kyuubi.server.thrift.resultset.default.fetch.size | 1000 | The number of rows sent in one Fetch RPC call by the server to the client, if not specified by the client. Respect `hive.server2.thrift.resultset.default.fetch.size` hive conf. | int | 1.9.1 | +| Key | Default | Meaning | Type | Since | +|----------------------------------------------------------|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|--------| +| kyuubi.server.administrators || Comma-separated list of Kyuubi service administrators. We use this config to grant admin permission to any service accounts when security mechanism is enabled. Note, when kyuubi.authentication is configured to NOSASL or NONE, everyone is treated as administrator. | set | 1.8.0 | +| kyuubi.server.conf.retrieveMode | REDACTED | Controls how session configurations are returned in REST API responses. Supported values:
    • REDACTED: Mask values that match kyuubi.server.redaction.regex (default).
    • ORIGINAL: Return the raw config values as-is.
    • NONE: Omit the conf map from responses entirely.
    | string | 1.12.0 | +| kyuubi.server.info.provider | ENGINE | The server information provider name, some clients may rely on this information to check the server compatibilities and functionalities.
  • SERVER: Return Kyuubi server information.
  • ENGINE: Return Kyuubi engine information.
  • | string | 1.6.1 | +| kyuubi.server.limit.batch.connections.per.ipaddress | <undefined> | Maximum kyuubi server batch connections per ipaddress. Any user exceeding this limit will not be allowed to connect. | int | 1.7.0 | +| kyuubi.server.limit.batch.connections.per.user | <undefined> | Maximum kyuubi server batch connections per user. Any user exceeding this limit will not be allowed to connect. | int | 1.7.0 | +| kyuubi.server.limit.batch.connections.per.user.ipaddress | <undefined> | Maximum kyuubi server batch connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect. | int | 1.7.0 | +| kyuubi.server.limit.client.fetch.max.rows | <undefined> | Max rows limit for getting result row set operation. If the max rows specified by client-side is larger than the limit, request will fail directly. | int | 1.8.0 | +| kyuubi.server.limit.connections.ip.deny.list || The client ip in the deny list will be denied to connect to kyuubi server. | set | 1.9.1 | +| kyuubi.server.limit.connections.per.ipaddress | <undefined> | Maximum kyuubi server connections per ipaddress. Any user exceeding this limit will not be allowed to connect. | int | 1.6.0 | +| kyuubi.server.limit.connections.per.user | <undefined> | Maximum kyuubi server connections per user. Any user exceeding this limit will not be allowed to connect. | int | 1.6.0 | +| kyuubi.server.limit.connections.per.user.ipaddress | <undefined> | Maximum kyuubi server connections per user:ipaddress combination. Any user-ipaddress exceeding this limit will not be allowed to connect. | int | 1.6.0 | +| kyuubi.server.limit.connections.user.deny.list || The user in the deny list will be denied to connect to kyuubi server, if the user has configured both user.unlimited.list and user.deny.list, the priority of the latter is higher. | set | 1.8.0 | +| kyuubi.server.limit.connections.user.unlimited.list || The maximum connections of the user in the white list will not be limited. | set | 1.7.0 | +| kyuubi.server.name | <undefined> | The name of Kyuubi Server. | string | 1.5.0 | +| kyuubi.server.periodicGC.interval | PT30M | How often to trigger the periodic garbage collection. 0 will disable it. | duration | 1.7.0 | +| kyuubi.server.redaction.regex | <undefined> | Regex to decide which Kyuubi contain sensitive information. When this regex matches a property key or value, the value is redacted from the various logs. || 1.6.0 | +| kyuubi.server.tempFile.expireTime | P30D | Expiration timout for cleanup server-side temporary files, e.g. operation logs. | duration | 1.10.0 | +| kyuubi.server.tempFile.maxCount | <undefined> | The upper threshold size of server-side temporary file paths to cleanup | int | 1.10.0 | +| kyuubi.server.thrift.resultset.default.fetch.size | 1000 | The number of rows sent in one Fetch RPC call by the server to the client, if not specified by the client. Respect `hive.server2.thrift.resultset.default.fetch.size` hive conf. | int | 1.9.1 | ### Session diff --git a/docs/deployment/migration-guide.md b/docs/deployment/migration-guide.md index d94f60984a5..59d0d3d5894 100644 --- a/docs/deployment/migration-guide.md +++ b/docs/deployment/migration-guide.md @@ -21,6 +21,7 @@ * Since Kyuubi 1.12, the support of variable `` substitution in config `kyuubi.metadata.store.jdbc.url` is deprecated, use `{{KYUUBI_HOME}}` instead. * Since Kyuubi 1.12, default value of `kyuubi.metrics.json.location` is changed to `{{KYUUBI_HOME}}/metrics`, to restore previous behavior, change it to `{{KYUUBI_WORK_DIR_ROOT}}/metrics`. +* Since Kyuubi 1.12, `GET /api/v1/sessions` returns only sessions owned by the authenticated user instead of all sessions on the server. To restore the previous behavior, set `kyuubi.frontend.rest.legacy.v1.sessionsReturnAllUsers=true`. ## Upgrading from Kyuubi 1.10 to 1.11 diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala index 9d389b0f360..c45d95cc3cc 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala @@ -3336,8 +3336,8 @@ object KyuubiConf { .regexConf .createOptional - val SESSION_CONF_DISPLAY_MODE: ConfigEntry[String] = - buildConf("kyuubi.session.conf.display.mode") + val SERVER_CONF_RETRIEVE_MODE: ConfigEntry[String] = + buildConf("kyuubi.server.conf.retrieveMode") .serverOnly .doc("Controls how session configurations are returned in REST API responses. " + "Supported values: " + @@ -3351,6 +3351,17 @@ object KyuubiConf { .checkValues(Set("REDACTED", "ORIGINAL", "NONE")) .createWithDefault("REDACTED") + val FRONTEND_REST_SESSION_LIST_LEGACY_MODE: ConfigEntry[Boolean] = + buildConf("kyuubi.frontend.rest.legacy.v1.sessionsReturnAllUsers") + .serverOnly + .doc("When true, GET /api/v1/sessions returns all sessions on the server regardless " + + "of the calling user (legacy behavior). When false (default), only sessions owned " + + "by the authenticated user are returned. " + + "This flag is provided for backward compatibility and will be removed in a future release.") + .version("1.12.0") + .booleanConf + .createWithDefault(false) + val SERVER_PERIODIC_GC_INTERVAL: ConfigEntry[Long] = buildConf("kyuubi.server.periodicGC.interval") .doc("How often to trigger the periodic garbage collection. 0 will disable it.") diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala index 4f5478376ba..3cfb20ae04b 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/ApiUtils.scala @@ -22,20 +22,25 @@ import scala.collection.JavaConverters._ import org.apache.kyuubi.{Logging, Utils} import org.apache.kyuubi.client.api.v1.dto import org.apache.kyuubi.client.api.v1.dto.{OperationData, OperationProgress, ServerData, SessionData} -import org.apache.kyuubi.config.KyuubiConf.{SERVER_SECRET_REDACTION_PATTERN, SESSION_CONF_DISPLAY_MODE} +import org.apache.kyuubi.config.KyuubiConf.{SERVER_CONF_RETRIEVE_MODE, SERVER_SECRET_REDACTION_PATTERN} import org.apache.kyuubi.ha.client.ServiceNodeInfo import org.apache.kyuubi.operation.KyuubiOperation import org.apache.kyuubi.session.KyuubiSession +object ConfRetrieveMode extends Enumeration { + val REDACTED, ORIGINAL, NONE = Value +} + object ApiUtils extends Logging { private def buildConf( rawConf: Map[String, String], session: KyuubiSession): java.util.Map[String, String] = { - session.sessionManager.getConf.get(SESSION_CONF_DISPLAY_MODE) match { - case "NONE" => Map.empty[String, String].asJava - case "ORIGINAL" => rawConf.asJava - case _ => + ConfRetrieveMode.withName( + session.sessionManager.getConf.get(SERVER_CONF_RETRIEVE_MODE)) match { + case ConfRetrieveMode.NONE => Map.empty[String, String].asJava + case ConfRetrieveMode.ORIGINAL => rawConf.asJava + case ConfRetrieveMode.REDACTED => val pattern = session.sessionManager.getConf.get(SERVER_SECRET_REDACTION_PATTERN) Utils.redact(pattern, rawConf.toSeq).toMap.asJava } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala index d165599f05f..d6e10c18805 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala @@ -32,6 +32,7 @@ import org.apache.commons.lang3.StringUtils import org.apache.kyuubi.Logging import org.apache.kyuubi.client.api.v1.dto import org.apache.kyuubi.client.api.v1.dto._ +import org.apache.kyuubi.config.KyuubiConf.FRONTEND_REST_SESSION_LIST_LEGACY_MODE import org.apache.kyuubi.config.KyuubiReservedKeys._ import org.apache.kyuubi.operation.{KyuubiOperation, OperationHandle} import org.apache.kyuubi.server.api.{ApiRequestContext, ApiUtils} @@ -55,12 +56,15 @@ private[v1] class SessionsResource extends ApiRequestContext with Logging { description = "get the list of live sessions for the current user") @GET def sessions(): Seq[SessionData] = { - val userName = fe.getSessionUser(Map.empty[String, String]) - sessionManager - .allSessions() - .filter(session => session.user == userName) - .map(session => ApiUtils.sessionData(session.asInstanceOf[KyuubiSession])) - .toSeq + val legacyMode = sessionManager.getConf.get(FRONTEND_REST_SESSION_LIST_LEGACY_MODE) + val allSessions = sessionManager.allSessions() + val filtered = + if (legacyMode) allSessions + else { + val userName = fe.getSessionUser(Map.empty[String, String]) + allSessions.filter(session => session.user == userName) + } + filtered.map(session => ApiUtils.sessionData(session.asInstanceOf[KyuubiSession])).toSeq } @ApiResponse( diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala index d194f603a65..aee80fb4c8a 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala @@ -461,7 +461,7 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { } private def withSessionConfDisplayMode(mode: String)(f: => Unit): Unit = { - conf.set(KyuubiConf.SESSION_CONF_DISPLAY_MODE, mode) - try f finally conf.set(KyuubiConf.SESSION_CONF_DISPLAY_MODE, "REDACTED") + conf.set(KyuubiConf.SERVER_CONF_RETRIEVE_MODE, mode) + try f finally conf.set(KyuubiConf.SERVER_CONF_RETRIEVE_MODE, "REDACTED") } } From 18a9b3e04298c49d515fd67dd59a250593ab2ce7 Mon Sep 17 00:00:00 2001 From: "Han, Yutong" Date: Wed, 15 Apr 2026 23:03:29 -0700 Subject: [PATCH 6/6] update migration guide as well as format change --- docs/deployment/migration-guide.md | 1 + .../apache/kyuubi/server/api/v1/SessionsResourceSuite.scala | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/deployment/migration-guide.md b/docs/deployment/migration-guide.md index f7125913b9e..225f37cc87e 100644 --- a/docs/deployment/migration-guide.md +++ b/docs/deployment/migration-guide.md @@ -21,6 +21,7 @@ * Since Kyuubi 1.12, the support of variable `` substitution in config `kyuubi.metadata.store.jdbc.url` is deprecated, use `{{KYUUBI_HOME}}` instead. * Since Kyuubi 1.12, default value of `kyuubi.metrics.json.location` is changed to `{{KYUUBI_HOME}}/metrics`, to restore previous behavior, change it to `{{KYUUBI_WORK_DIR_ROOT}}/metrics`. +* Since Kyuubi 1.12, session configurations in REST API responses are redacted by default using `kyuubi.server.redaction.regex`. Use `kyuubi.server.conf.retrieveMode` to control this behavior: `REDACTED` (default), `ORIGINAL` (no redaction), or `NONE` (omit configs entirely). * Since Kyuubi 1.12, `GET /api/v1/sessions` returns only sessions owned by the authenticated user instead of all sessions on the server. To restore the previous behavior, set `kyuubi.frontend.rest.legacy.v1.sessionsReturnAllUsers=true`. * Since Kyuubi 1.12, the configuration `spark.sql.kyuubi.hive.connector.dropTableAsPurgeTable` is introduced by Kyuubi Spark Hive connector(KSHC) to control whether DROP TABLE command completely remove its data by skipping HDFS trash. The default value is false. To restore the legacy behavior, set it to true. diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala index aee80fb4c8a..add41df5b8f 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala @@ -462,6 +462,7 @@ class SessionsResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper { private def withSessionConfDisplayMode(mode: String)(f: => Unit): Unit = { conf.set(KyuubiConf.SERVER_CONF_RETRIEVE_MODE, mode) - try f finally conf.set(KyuubiConf.SERVER_CONF_RETRIEVE_MODE, "REDACTED") + try f + finally conf.set(KyuubiConf.SERVER_CONF_RETRIEVE_MODE, "REDACTED") } }