You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kyuubi.apache.org by ch...@apache.org on 2022/06/16 02:33:10 UTC
[incubator-kyuubi] branch master updated: [KYUUBI #886] Add HTTP transport mode support to KYUUBI - no Kerberos support
This is an automated email from the ASF dual-hosted git repository.
chengpan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-kyuubi.git
The following commit(s) were added to refs/heads/master by this push:
new 1ea245d25 [KYUUBI #886] Add HTTP transport mode support to KYUUBI - no Kerberos support
1ea245d25 is described below
commit 1ea245d254f2b6c8ef58bdfff9d457bf9307e567
Author: Mahmoud Bahaa <ma...@incorta.com>
AuthorDate: Thu Jun 16 10:33:00 2022 +0800
[KYUUBI #886] Add HTTP transport mode support to KYUUBI - no Kerberos support
Same as https://github.com/apache/incubator-kyuubi/pull/2815 but with Kerberos auth removed as was not complete nor tested. so thought to make a separated PR with no Kerberos support then we can add Kerberos support later on
### _Why are the changes needed?_
Add http transport protocol support to Kyuubi
main code was taken from:
1. https://github.com/SteNicholas/incubator-kyuubi/commit/25ac0731d7331616988026996ca918793af151e9
2. https://github.com/apache/hive/blob/branch-3.1/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpCLIService.java
2. https://github.com/apache/hive/blob/branch-3.1/service/src/java/org/apache/hive/service/cli/thrift/ThriftHttpServlet.java
### _How was this patch tested?_
- [ ] Add some test cases that check the changes thoroughly including negative and positive cases if possible
- [ ] Add screenshots for manual tests if appropriate
- [ ] [Run test](https://kyuubi.apache.org/docs/latest/develop_tools/testing.html#running-tests) locally before make a pull request
Closes #2833 from mahmoudbahaa/transport-mode-http-no-kerberos.
Closes #886
3a24abec [Mahmoud Bahaa] fix test cases
ce982af7 [Mahmoud Bahaa] [KYUUBI #886] Add HTTP transport mode support to KYUUBI - no kerberos support
b6371859 [SteNicholas] [KYUUBI #886] Add HTTP transport mode support to KYUUBI
36fb0760 [Mahmoud Bahaa] update .gitignore to include conf non template files
Lead-authored-by: Mahmoud Bahaa <ma...@incorta.com>
Co-authored-by: SteNicholas <pr...@163.com>
Signed-off-by: Cheng Pan <ch...@apache.org>
---
.gitignore | 3 +
docs/deployment/settings.md | 20 +-
.../org/apache/kyuubi/config/KyuubiConf.scala | 135 ++++++-
.../kyuubi/service/TBinaryFrontendService.scala | 2 +
.../apache/kyuubi/service/TFrontendService.scala | 16 +-
.../KyuubiAuthenticationFactory.scala | 26 ++
.../org/apache/kyuubi/config/KyuubiConfSuite.scala | 4 +
.../apache/kyuubi/operation/SparkQueryTests.scala | 4 +
.../apache/kyuubi/metrics/MetricsConstants.scala | 5 +
.../org/apache/kyuubi/server/KyuubiServer.scala | 1 +
.../kyuubi/server/KyuubiTHttpFrontendService.scala | 283 +++++++++++++++
.../kyuubi/server/http/ThriftHttpServlet.scala | 390 +++++++++++++++++++++
.../kyuubi/server/http/util/CookieSigner.scala | 89 +++++
.../kyuubi/server/http/util/HttpAuthUtils.scala | 97 +++++
.../kyuubi/server/http/util/SessionManager.scala | 89 +++++
...rationThriftHttpKerberosAndPlainAuthSuite.scala | 42 +++
.../KyuubiOperationThriftHttpPerUserSuite.scala | 33 ++
17 files changed, 1231 insertions(+), 8 deletions(-)
diff --git a/.gitignore b/.gitignore
index b36d791d1..d5b95c21e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,3 +74,6 @@ embedded_zookeeper/
**/operation_logs/
**/server_operation_logs/
**/engine_operation_logs/
+conf/log4j2.properties
+conf/kyuubi-defaults.conf
+conf/kyuubi-env.sh
diff --git a/docs/deployment/settings.md b/docs/deployment/settings.md
index abeecd1bb..7acd94c18 100644
--- a/docs/deployment/settings.md
+++ b/docs/deployment/settings.md
@@ -269,12 +269,30 @@ kyuubi.frontend.mysql.max.worker.threads|999|Maximum number of threads in the co
kyuubi.frontend.mysql.min.worker.threads|9|Minimum number of threads in the command execution thread pool for the MySQL frontend service|int|1.4.0
kyuubi.frontend.mysql.netty.worker.threads|<undefined>|Number of thread in the netty worker event loop of MySQL frontend service. Use min(cpu_cores, 8) in default.|int|1.4.0
kyuubi.frontend.mysql.worker.keepalive.time|PT1M|Time(ms) that an idle async thread of the command execution thread pool will wait for a new task to arrive before terminating in MySQL frontend service|duration|1.4.0
-kyuubi.frontend.protocols|THRIFT_BINARY|A comma separated list for all frontend protocols <ul> <li>THRIFT_BINARY - HiveServer2 compatible thrift binary protocol.</li> <li>REST - Kyuubi defined REST API(experimental).</li> <li>MYSQL - MySQL compatible text protocol(experimental).</li> </ul>|seq|1.4.0
+kyuubi.frontend.protocols|THRIFT_BINARY|A comma separated list for all frontend protocols <ul> <li>THRIFT_BINARY - HiveServer2 compatible thrift binary protocol.</li> <li>THRIFT_HTTP - HiveServer2 compatible thrift http protocol.</li> <li>REST - Kyuubi defined REST API(experimental).</li> <li>MYSQL - MySQL compatible text protocol(experimental).</li> </ul>|seq|1.4.0
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.thrift.backoff.slot.length|PT0.1S|Time to back off during login to the thrift frontend service.|duration|1.4.0
kyuubi.frontend.thrift.binary.bind.host|<undefined>|Hostname or IP of the machine on which to run the thrift frontend service via binary protocol.|string|1.4.0
kyuubi.frontend.thrift.binary.bind.port|10009|Port of the machine on which to run the thrift frontend service via binary protocol.|int|1.4.0
+kyuubi.frontend.thrift.http.allow.user.substitution|true|Allow alternate user to be specified as part of open connection request when using HTTP transport mode.|boolean|1.6.0
+kyuubi.frontend.thrift.http.bind.host|<undefined>|Hostname or IP of the machine on which to run the thrift frontend service via http protocol.|string|1.6.0
+kyuubi.frontend.thrift.http.bind.port|10010|Port of the machine on which to run the thrift frontend service via http protocol.|int|1.6.0
+kyuubi.frontend.thrift.http.compression.enabled|true|Enable thrift http compression via Jetty compression support|boolean|1.6.0
+kyuubi.frontend.thrift.http.cookie.auth.enabled|true|When true, Kyuubi in HTTP transport mode, will use cookie based authentication mechanism|boolean|1.6.0
+kyuubi.frontend.thrift.http.cookie.domain|<undefined>|Domain for the Kyuubi generated cookies|string|1.6.0
+kyuubi.frontend.thrift.http.cookie.is.httponly|true|HttpOnly attribute of the Kyuubi generated cookie.|boolean|1.6.0
+kyuubi.frontend.thrift.http.cookie.max.age|86400|Maximum age in seconds for server side cookie used by Kyuubi in HTTP mode.|int|1.6.0
+kyuubi.frontend.thrift.http.cookie.path|<undefined>|Path for the Kyuubi generated cookies|string|1.6.0
+kyuubi.frontend.thrift.http.max.idle.time|PT30M|Maximum idle time for a connection on the server when in HTTP mode.|duration|1.6.0
+kyuubi.frontend.thrift.http.path|cliservice|Path component of URL endpoint when in HTTP mode.|string|1.6.0
+kyuubi.frontend.thrift.http.request.header.size|6144|Request header size in bytes, when using HTTP transport mode. Jetty defaults used.|int|1.6.0
+kyuubi.frontend.thrift.http.response.header.size|6144|Response header size in bytes, when using HTTP transport mode. Jetty defaults used.|int|1.6.0
+kyuubi.frontend.thrift.http.ssl.keystore.password|<undefined>|SSL certificate keystore password.|string|1.6.0
+kyuubi.frontend.thrift.http.ssl.keystore.path|<undefined>|SSL certificate keystore location.|string|1.6.0
+kyuubi.frontend.thrift.http.ssl.protocol.blacklist|SSLv2,SSLv3|SSL Versions to disable when using HTTP transport mode.|string|1.6.0
+kyuubi.frontend.thrift.http.use.SSL|false|Set this to true for using SSL encryption in http mode.|boolean|1.6.0
+kyuubi.frontend.thrift.http.xsrf.filter.enabled|false|If enabled, Kyuubi will block any requests made to it over http if an X-XSRF-HEADER header is not present|boolean|1.6.0
kyuubi.frontend.thrift.login.timeout|PT20S|Timeout for Thrift clients during login to the thrift frontend service.|duration|1.4.0
kyuubi.frontend.thrift.max.message.size|104857600|Maximum message size in bytes a Kyuubi server will accept.|int|1.4.0
kyuubi.frontend.thrift.max.worker.threads|999|Maximum number of threads in the of frontend worker thread pool for the thrift frontend service|int|1.4.0
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 7d10dd8b4..8d4521e26 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
@@ -135,6 +135,8 @@ case class KyuubiConf(loadSysDefault: Boolean = true) extends Logging {
FRONTEND_BIND_PORT,
FRONTEND_THRIFT_BINARY_BIND_HOST,
FRONTEND_THRIFT_BINARY_BIND_PORT,
+ FRONTEND_THRIFT_HTTP_BIND_HOST,
+ FRONTEND_THRIFT_HTTP_BIND_PORT,
FRONTEND_REST_BIND_HOST,
FRONTEND_REST_BIND_PORT,
FRONTEND_MYSQL_BIND_HOST,
@@ -300,7 +302,7 @@ object KyuubiConf {
object FrontendProtocols extends Enumeration {
type FrontendProtocol = Value
- val THRIFT_BINARY, REST, MYSQL = Value
+ val THRIFT_BINARY, THRIFT_HTTP, REST, MYSQL = Value
}
val FRONTEND_PROTOCOLS: ConfigEntry[Seq[String]] =
@@ -308,6 +310,7 @@ object KyuubiConf {
.doc("A comma separated list for all frontend protocols " +
"<ul>" +
" <li>THRIFT_BINARY - HiveServer2 compatible thrift binary protocol.</li>" +
+ " <li>THRIFT_HTTP - HiveServer2 compatible thrift http protocol.</li>" +
" <li>REST - Kyuubi defined REST API(experimental).</li> " +
" <li>MYSQL - MySQL compatible text protocol(experimental).</li> " +
"</ul>")
@@ -349,6 +352,21 @@ object KyuubiConf {
.version("1.4.0")
.fallbackConf(FRONTEND_BIND_PORT)
+ val FRONTEND_THRIFT_HTTP_BIND_HOST: ConfigEntry[Option[String]] =
+ buildConf("kyuubi.frontend.thrift.http.bind.host")
+ .doc("Hostname or IP of the machine on which to run the thrift frontend service " +
+ "via http protocol.")
+ .version("1.6.0")
+ .fallbackConf(FRONTEND_BIND_HOST)
+
+ val FRONTEND_THRIFT_HTTP_BIND_PORT: ConfigEntry[Int] =
+ buildConf("kyuubi.frontend.thrift.http.bind.port")
+ .doc("Port of the machine on which to run the thrift frontend service via http protocol.")
+ .version("1.6.0")
+ .intConf
+ .checkValue(p => p == 0 || (p > 1024 && p < 65535), "Invalid Port number")
+ .createWithDefault(10010)
+
val FRONTEND_MIN_WORKER_THREADS: ConfigEntry[Int] =
buildConf("kyuubi.frontend.min.worker.threads")
.doc("(deprecated) Minimum number of threads in the of frontend worker thread pool for " +
@@ -434,6 +452,121 @@ object KyuubiConf {
.version("1.4.0")
.fallbackConf(FRONTEND_LOGIN_BACKOFF_SLOT_LENGTH)
+ val FRONTEND_THRIFT_HTTP_REQUEST_HEADER_SIZE: ConfigEntry[Int] =
+ buildConf("kyuubi.frontend.thrift.http.request.header.size")
+ .doc("Request header size in bytes, when using HTTP transport mode. Jetty defaults used.")
+ .version("1.6.0")
+ .intConf
+ .createWithDefault(6 * 1024)
+
+ val FRONTEND_THRIFT_HTTP_RESPONSE_HEADER_SIZE: ConfigEntry[Int] =
+ buildConf("kyuubi.frontend.thrift.http.response.header.size")
+ .doc("Response header size in bytes, when using HTTP transport mode. Jetty defaults used.")
+ .version("1.6.0")
+ .intConf
+ .createWithDefault(6 * 1024)
+
+ val FRONTEND_THRIFT_HTTP_MAX_IDLE_TIME: ConfigEntry[Long] =
+ buildConf("kyuubi.frontend.thrift.http.max.idle.time")
+ .doc("Maximum idle time for a connection on the server when in HTTP mode.")
+ .version("1.6.0")
+ .timeConf
+ .createWithDefault(Duration.ofSeconds(1800).toMillis)
+
+ val FRONTEND_THRIFT_HTTP_PATH: ConfigEntry[String] =
+ buildConf("kyuubi.frontend.thrift.http.path")
+ .doc("Path component of URL endpoint when in HTTP mode.")
+ .version("1.6.0")
+ .stringConf
+ .createWithDefault("cliservice")
+
+ val FRONTEND_THRIFT_HTTP_COMPRESSION_ENABLED: ConfigEntry[Boolean] =
+ buildConf("kyuubi.frontend.thrift.http.compression.enabled")
+ .doc("Enable thrift http compression via Jetty compression support")
+ .version("1.6.0")
+ .booleanConf
+ .createWithDefault(true)
+
+ val FRONTEND_THRIFT_HTTP_COOKIE_AUTH_ENABLED: ConfigEntry[Boolean] =
+ buildConf("kyuubi.frontend.thrift.http.cookie.auth.enabled")
+ .doc("When true, Kyuubi in HTTP transport mode, " +
+ "will use cookie based authentication mechanism")
+ .version("1.6.0")
+ .booleanConf
+ .createWithDefault(true)
+
+ val FRONTEND_THRIFT_HTTP_COOKIE_MAX_AGE: ConfigEntry[Int] =
+ buildConf("kyuubi.frontend.thrift.http.cookie.max.age")
+ .doc("Maximum age in seconds for server side cookie used by Kyuubi in HTTP mode.")
+ .version("1.6.0")
+ .intConf
+ .createWithDefault(86400)
+
+ val FRONTEND_THRIFT_HTTP_COOKIE_DOMAIN: OptionalConfigEntry[String] =
+ buildConf("kyuubi.frontend.thrift.http.cookie.domain")
+ .doc("Domain for the Kyuubi generated cookies")
+ .version("1.6.0")
+ .stringConf
+ .createOptional
+
+ val FRONTEND_THRIFT_HTTP_COOKIE_PATH: OptionalConfigEntry[String] =
+ buildConf("kyuubi.frontend.thrift.http.cookie.path")
+ .doc("Path for the Kyuubi generated cookies")
+ .version("1.6.0")
+ .stringConf
+ .createOptional
+
+ val FRONTEND_THRIFT_HTTP_COOKIE_IS_HTTPONLY: ConfigEntry[Boolean] =
+ buildConf("kyuubi.frontend.thrift.http.cookie.is.httponly")
+ .doc("HttpOnly attribute of the Kyuubi generated cookie.")
+ .version("1.6.0")
+ .booleanConf
+ .createWithDefault(true)
+
+ val FRONTEND_THRIFT_HTTP_XSRF_FILTER_ENABLED: ConfigEntry[Boolean] =
+ buildConf("kyuubi.frontend.thrift.http.xsrf.filter.enabled")
+ .doc("If enabled, Kyuubi will block any requests made to it over http " +
+ "if an X-XSRF-HEADER header is not present")
+ .version("1.6.0")
+ .booleanConf
+ .createWithDefault(false)
+
+ val FRONTEND_THRIFT_HTTP_USE_SSL: ConfigEntry[Boolean] =
+ buildConf("kyuubi.frontend.thrift.http.use.SSL")
+ .doc("Set this to true for using SSL encryption in http mode.")
+ .version("1.6.0")
+ .booleanConf
+ .createWithDefault(false)
+
+ val FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PATH: OptionalConfigEntry[String] =
+ buildConf("kyuubi.frontend.thrift.http.ssl.keystore.path")
+ .doc("SSL certificate keystore location.")
+ .version("1.6.0")
+ .stringConf
+ .createOptional
+
+ val FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PASSWORD: OptionalConfigEntry[String] =
+ buildConf("kyuubi.frontend.thrift.http.ssl.keystore.password")
+ .doc("SSL certificate keystore password.")
+ .version("1.6.0")
+ .stringConf
+ .createOptional
+
+ val FRONTEND_THRIFT_HTTP_SSL_PROTOCOL_BLACKLIST: ConfigEntry[String] =
+ buildConf("kyuubi.frontend.thrift.http.ssl.protocol.blacklist")
+ .doc("SSL Versions to disable when using HTTP transport mode.")
+ .version("1.6.0")
+ .stringConf
+ .createWithDefault("SSLv2,SSLv3")
+
+ val FRONTEND_THRIFT_HTTP_ALLOW_USER_SUBSTITUTION: ConfigEntry[Boolean] =
+ buildConf("kyuubi.frontend.thrift.http.allow.user.substitution")
+ .doc("Allow alternate user to be specified as part of open connection" +
+ " request when using HTTP transport mode.")
+ .version("1.6.0")
+ .booleanConf
+ .createWithDefault(true)
+
val AUTHENTICATION_METHOD: ConfigEntry[Seq[String]] = buildConf("kyuubi.authentication")
.doc("A comma separated list of client authentication types.<ul>" +
" <li>NOSASL: raw transport.</li>" +
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala
index 10373840a..6cf36a3c4 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TBinaryFrontendService.scala
@@ -42,6 +42,8 @@ abstract class TBinaryFrontendService(name: String)
* @note this is final because we don't want new implementations for engine to override this.
* and we shall simply set it to zero for randomly picking an available port
*/
+ final override protected lazy val serverHost: Option[String] =
+ conf.get(FRONTEND_THRIFT_BINARY_BIND_HOST)
final override protected lazy val portNum: Int = conf.get(FRONTEND_THRIFT_BINARY_BIND_PORT)
private var server: Option[TServer] = None
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala
index 8ed1e4068..c9a9e1d78 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala
@@ -31,7 +31,7 @@ import org.apache.thrift.transport.TTransport
import org.apache.kyuubi.{KyuubiSQLException, Logging, Utils}
import org.apache.kyuubi.Utils.stringifyException
-import org.apache.kyuubi.config.KyuubiConf.{FRONTEND_CONNECTION_URL_USE_HOSTNAME, FRONTEND_THRIFT_BINARY_BIND_HOST}
+import org.apache.kyuubi.config.KyuubiConf.FRONTEND_CONNECTION_URL_USE_HOSTNAME
import org.apache.kyuubi.operation.{FetchOrientation, OperationHandle}
import org.apache.kyuubi.service.authentication.KyuubiAuthenticationFactory
import org.apache.kyuubi.session.SessionHandle
@@ -48,12 +48,13 @@ abstract class TFrontendService(name: String)
private val started = new AtomicBoolean(false)
private lazy val _hadoopConf: Configuration = KyuubiHadoopUtils.newHadoopConf(conf)
private lazy val serverThread = new NamedThreadFactory(getName, false).newThread(this)
- private lazy val serverHost = conf.get(FRONTEND_THRIFT_BINARY_BIND_HOST)
+ protected def serverHost: Option[String]
protected def portNum: Int
protected lazy val serverAddr: InetAddress =
serverHost.map(InetAddress.getByName).getOrElse(Utils.findLocalInetAddress)
protected lazy val serverSocket = new ServerSocket(portNum, -1, serverAddr)
+ protected lazy val actualPort: Int = serverSocket.getLocalPort
protected lazy val authFactory: KyuubiAuthenticationFactory =
new KyuubiAuthenticationFactory(conf, isServer())
@@ -117,11 +118,11 @@ abstract class TFrontendService(name: String)
case None =>
serverAddr.getHostAddress
}
- val actualPort = serverSocket.getLocalPort
+
host + ":" + actualPort
}
- private def getProxyUser(
+ protected def getProxyUser(
sessionConf: java.util.Map[String, String],
ipAddress: String,
realUser: String): String = {
@@ -134,7 +135,7 @@ abstract class TFrontendService(name: String)
}
}
- private def getUserName(req: TOpenSessionReq): String = {
+ protected def getUserName(req: TOpenSessionReq): String = {
val realUser: String =
ServiceUtils.getShortName(authFactory.getRemoteUser.getOrElse(req.getUsername))
if (req.getConfiguration == null) {
@@ -143,6 +144,9 @@ abstract class TFrontendService(name: String)
getProxyUser(req.getConfiguration, authFactory.getIpAddress.orNull, realUser)
}
}
+ protected def getIpAddress: String = {
+ authFactory.getIpAddress.orNull
+ }
private def getMinVersion(versions: TProtocolVersion*): TProtocolVersion = {
versions.minBy(_.getValue)
@@ -153,7 +157,7 @@ abstract class TFrontendService(name: String)
val protocol = getMinVersion(SERVER_VERSION, req.getClient_protocol)
res.setServerProtocolVersion(protocol)
val userName = getUserName(req)
- val ipAddress = authFactory.getIpAddress.orNull
+ val ipAddress = getIpAddress
val configuration =
Option(req.getConfiguration).map(_.asScala.toMap).getOrElse(Map.empty[String, String])
val sessionHandle = be.openSession(
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala
index f943def86..fa019ebde 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala
@@ -32,11 +32,13 @@ import org.apache.thrift.transport.{TSaslServerTransport, TTransportException, T
import org.apache.kyuubi.{KyuubiSQLException, Logging}
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.config.KyuubiConf._
+import org.apache.kyuubi.service.authentication.AuthMethods.AuthMethod
import org.apache.kyuubi.service.authentication.AuthTypes._
class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) extends Logging {
private val authTypes = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName)
+ private val none = authTypes.contains(NONE)
private val noSasl = authTypes == Seq(NOSASL)
private val kerberosEnabled = authTypes.contains(KERBEROS)
private val plainAuthTypeOpt = authTypes.filterNot(_.equals(KERBEROS))
@@ -117,6 +119,30 @@ class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) ex
hadoopAuthServer.map(_.getRemoteAddress).map(_.getHostAddress)
.orElse(Option(TSetIpAddressProcessor.getUserIpAddress))
}
+
+ def isNoSaslEnabled: Boolean = {
+ noSasl
+ }
+
+ def isKerberosEnabled: Boolean = {
+ kerberosEnabled
+ }
+
+ def isPlainAuthEnabled: Boolean = {
+ plainAuthTypeOpt.isDefined
+ }
+
+ def isNoneEnabled: Boolean = {
+ none
+ }
+
+ def getValidPasswordAuthMethod: AuthMethod = {
+ debug(authTypes)
+ if (none) AuthMethods.NONE
+ else if (authTypes.contains(LDAP)) AuthMethods.LDAP
+ else if (authTypes.contains(CUSTOM)) AuthMethods.CUSTOM
+ else throw new IllegalArgumentException("No valid Password Auth detected")
+ }
}
object KyuubiAuthenticationFactory {
val HS2_PROXY_USER = "hive.server2.proxy.user"
diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/config/KyuubiConfSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/config/KyuubiConfSuite.scala
index 05cf23396..215c5327e 100644
--- a/kyuubi-common/src/test/scala/org/apache/kyuubi/config/KyuubiConfSuite.scala
+++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/config/KyuubiConfSuite.scala
@@ -58,6 +58,9 @@ class KyuubiConfSuite extends KyuubiFunSuite {
conf.set(FRONTEND_THRIFT_BINARY_BIND_HOST.key, "kentyao.org")
assert(conf.get(FRONTEND_THRIFT_BINARY_BIND_HOST).get === "kentyao.org")
+ conf.set(FRONTEND_THRIFT_HTTP_BIND_HOST.key, "kentyao.org")
+ assert(conf.get(FRONTEND_THRIFT_HTTP_BIND_HOST).get === "kentyao.org")
+
conf.setIfMissing(OPERATION_IDLE_TIMEOUT, 60L)
assert(conf.get(OPERATION_IDLE_TIMEOUT) === 5)
@@ -72,6 +75,7 @@ class KyuubiConfSuite extends KyuubiFunSuite {
val map = conf.getAllWithPrefix("kyuubi", "")
assert(map(FRONTEND_THRIFT_BINARY_BIND_HOST.key.substring(7)) === "kentyao.org")
+ assert(map(FRONTEND_THRIFT_HTTP_BIND_HOST.key.substring(7)) === "kentyao.org")
val map1 = conf.getAllWithPrefix("kyuubi", "operation")
assert(map1(OPERATION_IDLE_TIMEOUT.key.substring(7)) === "PT0.005S")
assert(map1.size === 1)
diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkQueryTests.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkQueryTests.scala
index ead80d98d..654d4afad 100644
--- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkQueryTests.scala
+++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/SparkQueryTests.scala
@@ -32,6 +32,8 @@ trait SparkQueryTests extends HiveJDBCTestHelper {
protected lazy val SPARK_ENGINE_MAJOR_MINOR_VERSION: (Int, Int) = sparkEngineMajorMinorVersion
+ protected lazy val httpMode = false;
+
test("execute statement - select null") {
withJdbcStatement() { statement =>
val resultSet = statement.executeQuery("SELECT NULL AS col")
@@ -332,6 +334,8 @@ trait SparkQueryTests extends HiveJDBCTestHelper {
}
test("execute statement - select with variable substitution") {
+ assume(!httpMode)
+
withThriftClient { client =>
val req = new TOpenSessionReq()
req.setUsername("chengpan")
diff --git a/kyuubi-metrics/src/main/scala/org/apache/kyuubi/metrics/MetricsConstants.scala b/kyuubi-metrics/src/main/scala/org/apache/kyuubi/metrics/MetricsConstants.scala
index 68116f4db..fcf7d56bc 100644
--- a/kyuubi-metrics/src/main/scala/org/apache/kyuubi/metrics/MetricsConstants.scala
+++ b/kyuubi-metrics/src/main/scala/org/apache/kyuubi/metrics/MetricsConstants.scala
@@ -31,11 +31,16 @@ object MetricsConstants {
final val EXEC_POOL_ACTIVE: String = KYUUBI + "exec.pool.threads.active"
final private val CONN = KYUUBI + "connection."
+ final private val THRIFT_HTTP_CONN = KYUUBI + "thrift.http.connection."
final val CONN_OPEN: String = CONN + "opened"
final val CONN_FAIL: String = CONN + "failed"
final val CONN_TOTAL: String = CONN + "total"
+ final val THRIFT_HTTP_CONN_OPEN: String = THRIFT_HTTP_CONN + "opened"
+ final val THRIFT_HTTP_CONN_FAIL: String = THRIFT_HTTP_CONN + "failed"
+ final val THRIFT_HTTP_CONN_TOTAL: String = THRIFT_HTTP_CONN + "total"
+
final private val ENGINE = KYUUBI + "engine."
final val ENGINE_FAIL: String = ENGINE + "failed"
final val ENGINE_TIMEOUT: String = ENGINE + "timeout"
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
index 59f1fa26b..1ccae0370 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
@@ -117,6 +117,7 @@ class KyuubiServer(name: String) extends Serverable(name) {
override lazy val frontendServices: Seq[AbstractFrontendService] =
conf.get(FRONTEND_PROTOCOLS).map(FrontendProtocols.withName).map {
case THRIFT_BINARY => new KyuubiTBinaryFrontendService(this)
+ case THRIFT_HTTP => new KyuubiTHttpFrontendService(this)
case REST =>
warn("REST frontend protocol is experimental, API may change in the future.")
new KyuubiRestFrontendService(this)
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala
new file mode 100644
index 000000000..565589010
--- /dev/null
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala
@@ -0,0 +1,283 @@
+/*
+ * 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.kyuubi.server
+
+import java.net.ServerSocket
+import java.util.concurrent.{SynchronousQueue, ThreadPoolExecutor, TimeUnit}
+import javax.security.sasl.AuthenticationException
+import javax.servlet.{ServletContextEvent, ServletContextListener}
+
+import org.apache.commons.lang3.SystemUtils
+import org.apache.hive.service.rpc.thrift.{TCLIService, TOpenSessionReq}
+import org.apache.thrift.protocol.TBinaryProtocol
+import org.eclipse.jetty.http.HttpMethod
+import org.eclipse.jetty.security.{ConstraintMapping, ConstraintSecurityHandler}
+import org.eclipse.jetty.server._
+import org.eclipse.jetty.server.handler.gzip.GzipHandler
+import org.eclipse.jetty.servlet.{ServletContextHandler, ServletHolder}
+import org.eclipse.jetty.util.security.Constraint
+import org.eclipse.jetty.util.ssl.SslContextFactory
+import org.eclipse.jetty.util.thread.ExecutorThreadPool
+
+import org.apache.kyuubi.KyuubiException
+import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.config.KyuubiConf._
+import org.apache.kyuubi.metrics.MetricsConstants.{THRIFT_HTTP_CONN_FAIL, THRIFT_HTTP_CONN_OPEN, THRIFT_HTTP_CONN_TOTAL}
+import org.apache.kyuubi.metrics.MetricsSystem
+import org.apache.kyuubi.server.http.ThriftHttpServlet
+import org.apache.kyuubi.server.http.util.SessionManager
+import org.apache.kyuubi.service.{Serverable, Service, ServiceUtils, TFrontendService}
+import org.apache.kyuubi.util.NamedThreadFactory
+
+/**
+ * Apache Thrift based hive service rpc
+ * 1. the server side implementation serves client-server rpc calls
+ * 2. the engine side implementations serve server-engine rpc calls
+ */
+final class KyuubiTHttpFrontendService(
+ override val serverable: Serverable)
+ extends TFrontendService("KyuubiTHttpFrontendService") {
+
+ override protected lazy val serverHost: Option[String] =
+ conf.get(FRONTEND_THRIFT_HTTP_BIND_HOST)
+ override protected lazy val portNum: Int = conf.get(FRONTEND_THRIFT_HTTP_BIND_PORT)
+ override protected lazy val actualPort: Int = portNum
+ override protected lazy val serverSocket: ServerSocket = null
+
+ private var server: Option[Server] = None
+ private val APPLICATION_THRIFT = "application/x-thrift"
+
+ /**
+ * Configure Jetty to serve http requests. Example of a client connection URL:
+ * http://localhost:10000/servlets/thrifths2/ A gateway may cause actual target
+ * URL to differ, e.g. http://gateway:port/hive2/servlets/thrifths2/.
+ *
+ * @param conf the configuration of the service
+ */
+ override def initialize(conf: KyuubiConf): Unit = synchronized {
+ this.conf = conf
+ if (authFactory.isKerberosEnabled) {
+ try {
+ authFactory.getValidPasswordAuthMethod
+ } catch {
+ case _: IllegalArgumentException =>
+ throw new AuthenticationException("Kerberos is not supported for thrift http mode")
+ }
+ }
+
+ try {
+ // Server thread pool
+ // Start with minWorkerThreads, expand till maxWorkerThreads and reject
+ // subsequent requests
+ val minThreads = conf.get(FRONTEND_THRIFT_MIN_WORKER_THREADS)
+ val maxThreads = conf.get(FRONTEND_THRIFT_MAX_WORKER_THREADS)
+ val keepAliveTime = conf.get(FRONTEND_THRIFT_WORKER_KEEPALIVE_TIME)
+ val executor = new ThreadPoolExecutor(
+ minThreads,
+ maxThreads,
+ keepAliveTime,
+ TimeUnit.MILLISECONDS,
+ new SynchronousQueue[Runnable](),
+ new NamedThreadFactory(getName + "HttpHandler-Pool", false))
+ val threadPool = new ExecutorThreadPool(executor)
+
+ // HTTP Server
+ server = Some(new Server(threadPool))
+
+ val httpConf = new HttpConfiguration
+ // Configure header size
+ val requestHeaderSize = conf.get(FRONTEND_THRIFT_HTTP_REQUEST_HEADER_SIZE)
+ val responseHeaderSize = conf.get(FRONTEND_THRIFT_HTTP_RESPONSE_HEADER_SIZE)
+ httpConf.setRequestHeaderSize(requestHeaderSize)
+ httpConf.setResponseHeaderSize(responseHeaderSize)
+ val connectionFactory = new HttpConnectionFactory(httpConf)
+
+ val useSsl = conf.get(FRONTEND_THRIFT_HTTP_USE_SSL)
+ val schemeName = if (useSsl) "https" else "http"
+
+ // Change connector if SSL is used
+ val connector =
+ if (useSsl) {
+ val keyStorePath = conf.get(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PATH)
+
+ if (keyStorePath.isEmpty) {
+ throw new IllegalArgumentException(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PATH.key +
+ " Not configured for SSL connection, please set the key with: " +
+ FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PATH.doc)
+ }
+
+ val keyStorePassword = conf.get(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PASSWORD)
+ if (keyStorePassword.isEmpty) {
+ throw new IllegalArgumentException(FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PASSWORD.key +
+ " Not configured for SSL connection. please set the key with: " +
+ FRONTEND_THRIFT_HTTP_SSL_KEYSTORE_PASSWORD.doc)
+ }
+
+ val sslContextFactory = new SslContextFactory.Server
+ val excludedProtocols = conf.get(FRONTEND_THRIFT_HTTP_SSL_PROTOCOL_BLACKLIST).split(",")
+ info("Thrift HTTP Server SSL: adding excluded protocols: " +
+ String.join(",", excludedProtocols: _*))
+ sslContextFactory.addExcludeProtocols(excludedProtocols: _*)
+ info("Thrift HTTP Server SSL: SslContextFactory.getExcludeProtocols = " +
+ String.join(",", sslContextFactory.getExcludeProtocols: _*))
+ sslContextFactory.setKeyStorePath(keyStorePath.get)
+ sslContextFactory.setKeyStorePassword(keyStorePassword.get)
+ new ServerConnector(
+ server.get,
+ sslContextFactory,
+ connectionFactory)
+ } else {
+ new ServerConnector(server.get, connectionFactory)
+ }
+
+ connector.setPort(portNum)
+ // Linux:yes, Windows:no
+ // result of setting the SO_REUSEADDR flag is different on Windows
+ // http://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx
+ // without this 2 NN's can start on the same machine and listen on
+ // the same port with indeterminate routing of incoming requests to them
+ connector.setReuseAddress(!SystemUtils.IS_OS_WINDOWS)
+ val maxIdleTime = conf.get(FRONTEND_THRIFT_HTTP_MAX_IDLE_TIME)
+ connector.setIdleTimeout(maxIdleTime)
+ connector.setAcceptQueueSize(maxThreads)
+ server.foreach(_.addConnector(connector))
+
+ val processor = new TCLIService.Processor[TCLIService.Iface](this)
+ val protocolFactory = new TBinaryProtocol.Factory
+ val servlet = new ThriftHttpServlet(processor, protocolFactory, authFactory, conf)
+ servlet.init()
+
+ // Context handler
+ val context = new ServletContextHandler(ServletContextHandler.SESSIONS)
+ context.setContextPath("/")
+
+ context.addEventListener(new ServletContextListener() {
+ override def contextInitialized(servletContextEvent: ServletContextEvent): Unit = {
+ MetricsSystem.tracing { ms =>
+ ms.incCount(THRIFT_HTTP_CONN_TOTAL)
+ ms.incCount(THRIFT_HTTP_CONN_OPEN)
+ }
+ }
+
+ override def contextDestroyed(servletContextEvent: ServletContextEvent): Unit = {
+ MetricsSystem.tracing { ms =>
+ ms.decCount(THRIFT_HTTP_CONN_OPEN)
+ }
+ }
+ })
+
+ val httpPath = getHttpPath(conf.get(FRONTEND_THRIFT_HTTP_PATH))
+
+ if (conf.get(FRONTEND_THRIFT_HTTP_COMPRESSION_ENABLED)) {
+ val gzipHandler = new GzipHandler
+ gzipHandler.setHandler(context)
+ gzipHandler.addIncludedMethods(HttpMethod.POST.asString())
+ gzipHandler.addIncludedMimeTypes(APPLICATION_THRIFT)
+ server.foreach(_.setHandler(gzipHandler))
+ } else {
+ server.foreach(_.setHandler(context))
+ }
+
+ context.addServlet(new ServletHolder(servlet), httpPath)
+ constrainHttpMethods(context)
+
+ info(s"Started ${getClass.getSimpleName} in $schemeName mode on port $portNum " +
+ s"path=$httpPath with $minThreads ... $maxThreads threads")
+ } catch {
+ case e: Throwable =>
+ MetricsSystem.tracing(_.incCount(THRIFT_HTTP_CONN_FAIL))
+ error(e)
+ throw new KyuubiException(
+ s"Failed to initialize frontend service on $serverAddr:$portNum.",
+ e)
+ }
+ super.initialize(conf)
+ }
+
+ override def run(): Unit =
+ try {
+ if (isServer()) {
+ info(s"Starting and exposing JDBC connection at: jdbc:hive2://$connectionUrl/")
+ }
+ server.foreach(_.start())
+ } catch {
+ case _: InterruptedException => error(s"$getName is interrupted")
+ case t: Throwable =>
+ error(s"Error starting $getName", t)
+ System.exit(-1)
+ }
+
+ override protected def stopServer(): Unit = {
+ server.foreach(_.stop())
+ server = None
+ }
+
+ override protected def isServer(): Boolean = true
+
+ override val discoveryService: Option[Service] = None
+
+ private def getHttpPath(httpPath: String): String = {
+ if (httpPath == null || httpPath == "") return "/*"
+ else {
+ if (!httpPath.startsWith("/")) return "/" + httpPath
+ if (httpPath.endsWith("/")) return httpPath + "*"
+ if (!httpPath.endsWith("/*")) return httpPath + "/*"
+ }
+ httpPath
+ }
+
+ def constrainHttpMethods(ctxHandler: ServletContextHandler): Unit = {
+ val constraint = new Constraint
+ constraint.setAuthenticate(true)
+ val cmt = new ConstraintMapping
+ cmt.setConstraint(constraint)
+ cmt.setMethod("TRACE")
+ cmt.setPathSpec("/*")
+ val securityHandler = new ConstraintSecurityHandler
+ val cmo = new ConstraintMapping
+ cmo.setConstraint(constraint)
+ cmo.setMethod("OPTIONS")
+ cmo.setPathSpec("/*")
+ securityHandler.setConstraintMappings(Array[ConstraintMapping](cmt, cmo))
+ ctxHandler.setSecurityHandler(securityHandler)
+ }
+
+ override protected def getIpAddress: String = {
+ SessionManager.getIpAddress
+ }
+
+ override protected def getUserName(req: TOpenSessionReq): String = {
+ var userName: String = SessionManager.getUserName
+ if (userName == null) userName = req.getUsername
+ userName = getShortName(userName)
+ val effectiveClientUser: String = getProxyUser(req.getConfiguration, getIpAddress, userName)
+ debug("Client's username: " + effectiveClientUser)
+ effectiveClientUser
+ }
+
+ private def getShortName(userName: String): String = {
+ var ret: String = null
+
+ if (userName != null) {
+ val indexOfDomainMatch = ServiceUtils.indexOfDomainMatch(userName)
+ ret = if (indexOfDomainMatch <= 0) userName else userName.substring(0, indexOfDomainMatch)
+ }
+
+ ret
+ }
+}
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala
new file mode 100644
index 000000000..fc9fe563f
--- /dev/null
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala
@@ -0,0 +1,390 @@
+/*
+ * 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.kyuubi.server.http
+
+import java.io.IOException
+import java.security.SecureRandom
+import javax.security.sasl.AuthenticationException
+import javax.servlet.ServletException
+import javax.servlet.http.Cookie
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+import javax.ws.rs.core.NewCookie
+
+import scala.collection.mutable
+
+import org.apache.commons.codec.binary.Base64
+import org.apache.commons.codec.binary.StringUtils
+import org.apache.hadoop.hive.shims.Utils
+import org.apache.thrift.TProcessor
+import org.apache.thrift.protocol.TProtocolFactory
+import org.apache.thrift.server.TServlet
+
+import org.apache.kyuubi.Logging
+import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.server.http.util.{CookieSigner, HttpAuthUtils, SessionManager}
+import org.apache.kyuubi.service.authentication.{AuthenticationProviderFactory, KyuubiAuthenticationFactory}
+
+class ThriftHttpServlet(
+ processor: TProcessor,
+ protocolFactory: TProtocolFactory,
+ authFactory: KyuubiAuthenticationFactory,
+ conf: KyuubiConf)
+ extends TServlet(processor, protocolFactory) with Logging {
+ // Class members for cookie based authentication.
+ private var signer: CookieSigner = _
+ val AUTH_COOKIE = "hive.server2.auth"
+ private val RAN = new SecureRandom()
+ private var isCookieAuthEnabled: Boolean = false
+ private var cookieDomain: String = _
+ private var cookiePath: String = _
+ private var cookieMaxAge: Int = 0
+ private var isCookieSecure = false
+ private var isHttpOnlyCookie = false
+ private val X_FORWARDED_FOR_HEADER = "X-Forwarded-For"
+
+ override def init(): Unit = {
+ isCookieAuthEnabled = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_COOKIE_AUTH_ENABLED)
+ // Initialize the cookie based authentication related variables.
+ if (isCookieAuthEnabled) { // Generate the signer with secret.
+ val secret = RAN.nextLong().toString
+ debug("Using the random number as the secret for cookie generation " + secret)
+ signer = new CookieSigner(secret.getBytes)
+ cookieMaxAge = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_COOKIE_MAX_AGE)
+ cookieDomain = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_COOKIE_DOMAIN).orNull
+ cookiePath = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_COOKIE_PATH).orNull
+ // always send secure cookies for SSL mode
+ isCookieSecure = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_USE_SSL)
+ isHttpOnlyCookie = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_COOKIE_IS_HTTPONLY)
+ }
+ }
+
+ @throws[ServletException]
+ @throws[IOException]
+ override def doPost(request: HttpServletRequest, response: HttpServletResponse): Unit = {
+ var clientUserName: String = null
+ var clientIpAddress: String = null
+ var requireNewCookie: Boolean = false
+ try {
+ if (conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_XSRF_FILTER_ENABLED)) {
+ val continueProcessing = Utils.doXsrfFilter(request, response, null, null)
+ if (!continueProcessing) {
+ warn("Request did not have valid XSRF header, rejecting.")
+ return
+ }
+ }
+
+ // If the cookie based authentication is already enabled, parse the
+ // request and validate the request cookies.
+ if (isCookieAuthEnabled) {
+ debug("Cookie Auth Enabled")
+ clientUserName = validateCookie(request)
+ requireNewCookie = clientUserName == null
+ if (requireNewCookie) {
+ info("Could not validate cookie sent, will try to generate a new cookie")
+ } else {
+ debug("Got userName From Cookie: " + clientUserName)
+ }
+ }
+
+ // If the cookie based authentication is not enabled or the request does not have a valid
+ // cookie, use authentication depending on the server setup.
+ if (clientUserName == null) {
+ clientUserName = doPasswdAuth(request, authFactory)
+ }
+
+ assert(clientUserName != null)
+ debug("Client username: " + clientUserName)
+
+ // Set the thread local username to be used for doAs if true
+ SessionManager.setUserName(clientUserName)
+
+ // find proxy user if any from query param
+ val doAsQueryParam = getDoAsQueryParam(request.getQueryString)
+ if (doAsQueryParam != null) SessionManager.setProxyUserName(doAsQueryParam)
+
+ clientIpAddress = request.getRemoteAddr
+ debug("Client IP Address: " + clientIpAddress)
+ // Set the thread local ip address
+ SessionManager.setIpAddress(clientIpAddress)
+
+ val forwarded_for = request.getHeader(X_FORWARDED_FOR_HEADER)
+ if (forwarded_for != null) {
+ debug(X_FORWARDED_FOR_HEADER + ":" + forwarded_for)
+ val forwardedAddresses = forwarded_for.split(",").toList
+ SessionManager.setForwardedAddresses(forwardedAddresses)
+ } else SessionManager.setForwardedAddresses(List.empty[String])
+
+ // Generate new cookie and add it to the response
+ if (requireNewCookie && !authFactory.isNoSaslEnabled) {
+ val cookieToken = HttpAuthUtils.createCookieToken(clientUserName)
+ val hs2Cookie = createCookie(signer.signCookie(cookieToken))
+ if (isHttpOnlyCookie) response.setHeader("SET-COOKIE", getHttpOnlyCookieHeader(hs2Cookie))
+ else response.addCookie(hs2Cookie)
+ info("Cookie added for clientUserName " + clientUserName)
+ }
+ super.doPost(request, response)
+ } catch {
+ case e: AuthenticationException =>
+ error("Error: ", e)
+ // Send a 401 to the client
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
+
+ // scalastyle:off println
+ response.getWriter.println("Authentication Error: " + e.getMessage)
+ // scalastyle:on println
+ case e: Throwable =>
+ error("Error: ", e)
+ throw e
+ } finally {
+ // Clear the thread locals
+ SessionManager.clearUserName()
+ SessionManager.clearIpAddress()
+ SessionManager.clearProxyUserName()
+ SessionManager.clearForwardedAddresses()
+ }
+ }
+
+ /**
+ * Retrieves the client name from cookieString. If the cookie does not
+ * correspond to a valid client, the function returns null.
+ *
+ * @param cookies HTTP Request cookies.
+ * @return Client Username if cookieString has a HS2 Generated cookie that is currently valid.
+ * Else, returns null.
+ */
+ private def getClientNameFromCookie(cookies: Array[Cookie]): String = {
+ // Current Cookie Name, Current Cookie Value
+ var currName: String = null
+ var currValue: String = null
+ // Following is the main loop which iterates through all the cookies send by the client.
+ // The HS2 generated cookies are of the format hive.server2.auth=<value>
+ // A cookie which is identified as a hiveserver2 generated cookie is validated
+ // by calling signer.verifyAndExtract(). If the validation passes, send the
+ // username for which the cookie is validated to the caller. If no client side
+ // cookie passes the validation, return null to the caller.
+ for (currCookie <- cookies) { // Get the cookie name
+ currName = currCookie.getName
+ if (currName == AUTH_COOKIE) {
+ // If we reached here, we have match for HS2 generated cookie
+ currValue = currCookie.getValue
+ // Validate the value.
+ try currValue = signer.verifyAndExtract(currValue)
+ catch {
+ case e: IllegalArgumentException =>
+ debug("Invalid cookie" + e.getMessage)
+ currValue = null
+ }
+ // Retrieve the user name, do the final validation step.
+ if (currValue != null) {
+ val userName = HttpAuthUtils.getUserNameFromCookieToken(currValue)
+ if (userName == null) {
+ warn("Invalid cookie token " + currValue)
+ } else {
+ // We have found a valid cookie in the client request.
+ debug("Validated the cookie for user " + userName)
+ return userName
+ }
+ }
+ }
+ }
+ // No valid HS2 generated cookies found, return null
+ null
+ }
+
+ /**
+ * Convert cookie array to human readable cookie string
+ *
+ * @param cookies Cookie Array
+ * @return String containing all the cookies separated by a newline character.
+ * Each cookie is of the format [key]=[value]
+ */
+ private def toCookieStr(cookies: Array[Cookie]) = {
+ val cookieStr = new mutable.StringBuilder
+ for (c <- cookies) {
+ cookieStr.append(c.getName).append('=').append(c.getValue).append(" ;\n")
+ }
+ cookieStr.toString
+ }
+
+ /**
+ * Validate the request cookie. This function iterates over the request cookie headers
+ * and finds a cookie that represents a valid client/server session. If it finds one, it
+ * returns the client name associated with the session. Else, it returns null.
+ *
+ * @param request The HTTP Servlet Request send by the client
+ * @return Client Username if the request has valid HS2 cookie, else returns null
+ */
+ private def validateCookie(request: HttpServletRequest): String = {
+ // Find all the valid cookies associated with the request.
+ val cookies = request.getCookies
+ if (cookies == null) {
+ debug("No valid cookies associated with the request " + request)
+ return null
+ }
+ debug("Received cookies: " + toCookieStr(cookies))
+ getClientNameFromCookie(cookies)
+ }
+
+ import java.io.UnsupportedEncodingException
+
+ /**
+ * Generate a server side cookie given the cookie value as the input.
+ *
+ * @param str Input string token.
+ * @return The generated cookie.
+ * @throws UnsupportedEncodingException
+ */
+ @throws[UnsupportedEncodingException]
+ private def createCookie(str: String): Cookie = {
+ debug("Cookie name = " + AUTH_COOKIE + " value = " + str)
+ val cookie = new Cookie(AUTH_COOKIE, str)
+ cookie.setMaxAge(cookieMaxAge)
+ if (cookieDomain != null) cookie.setDomain(cookieDomain)
+ if (cookiePath != null) cookie.setPath(cookiePath)
+ cookie.setSecure(isCookieSecure)
+ cookie
+ }
+
+ /**
+ * Generate httponly cookie from HS2 cookie
+ *
+ * @param cookie HS2 generated cookie
+ * @return The httponly cookie
+ */
+ private def getHttpOnlyCookieHeader(cookie: Cookie): String = {
+ val newCookie = new NewCookie(
+ cookie.getName,
+ cookie.getValue,
+ cookie.getPath,
+ cookie.getDomain,
+ cookie.getVersion,
+ cookie.getComment,
+ cookie.getMaxAge,
+ cookie.getSecure)
+ newCookie + "; HttpOnly"
+ }
+
+ /**
+ * Do the LDAP/PAM authentication
+ *
+ * @param request
+ * @param authFactory
+ * @throws AuthenticationException
+ */
+ @throws[AuthenticationException]
+ private def doPasswdAuth(
+ request: HttpServletRequest,
+ authFactory: KyuubiAuthenticationFactory): String = {
+ val userName = getUsername(request, authFactory: KyuubiAuthenticationFactory)
+ debug("Is No SASL Enabled : " + authFactory.isNoSaslEnabled)
+ // No-op when authType is NOSASL
+ if (authFactory.isNoSaslEnabled) return userName
+ try {
+ debug("Initiating Password Authentication")
+ val password = getPassword(request, authFactory)
+ val authMethod = authFactory.getValidPasswordAuthMethod
+ debug("Password Method: " + authMethod)
+ val provider = AuthenticationProviderFactory.getAuthenticationProvider(authMethod, conf)
+ debug("Password Provider obtained")
+ provider.authenticate(userName, password)
+ debug("Password Provider authenticated username successfully")
+ } catch {
+ case e: Exception =>
+ throw new AuthenticationException(e.getMessage, e)
+ }
+ userName
+ }
+
+ @throws[AuthenticationException]
+ private def getUsername(
+ request: HttpServletRequest,
+ authFactory: KyuubiAuthenticationFactory): String = {
+ val creds = getAuthHeaderTokens(request, authFactory)
+ // Username must be present
+ if (creds(0) == null || creds(0).isEmpty) {
+ throw new AuthenticationException("Authorization header received " +
+ "from the client does not contain username.")
+ }
+ creds(0)
+ }
+
+ @throws[AuthenticationException]
+ private def getPassword(
+ request: HttpServletRequest,
+ authFactory: KyuubiAuthenticationFactory): String = {
+ val creds = getAuthHeaderTokens(request, authFactory)
+ // Password must be present
+ if (creds(1) == null || creds(1).isEmpty) {
+ throw new AuthenticationException("Authorization header received " +
+ "from the client does not contain username.")
+ }
+ creds(1)
+ }
+
+ @throws[AuthenticationException]
+ private def getAuthHeaderTokens(
+ request: HttpServletRequest,
+ authFactory: KyuubiAuthenticationFactory): Array[String] = {
+ val authHeaderBase64 = getAuthHeader(request, authFactory)
+ val authHeaderString = StringUtils.newStringUtf8(Base64.decodeBase64(authHeaderBase64.getBytes))
+ authHeaderString.split(":")
+ }
+
+ /**
+ * Returns the base64 encoded auth header payload
+ *
+ * @param request
+ * @param authFactory
+ * @return
+ * @throws AuthenticationException
+ */
+ @throws[AuthenticationException]
+ private def getAuthHeader(
+ request: HttpServletRequest,
+ authFactory: KyuubiAuthenticationFactory): String = {
+ val authHeader = request.getHeader(HttpAuthUtils.AUTHORIZATION)
+ // Each http request must have an Authorization header
+ if (authHeader == null || authHeader.isEmpty) {
+ throw new AuthenticationException("Authorization header received " +
+ "from the client is empty.")
+ }
+
+ var authHeaderBase64String: String = null
+ val beginIndex = (HttpAuthUtils.BASIC + " ").length
+ authHeaderBase64String = authHeader.substring(beginIndex)
+ // Authorization header must have a payload
+ if (authHeaderBase64String == null || authHeaderBase64String.isEmpty) {
+ throw new AuthenticationException("Authorization header received " +
+ "from the client does not contain any data.")
+ }
+ authHeaderBase64String
+ }
+
+ private def getDoAsQueryParam(queryString: String): String = {
+ debug("URL query string:" + queryString)
+ if (queryString == null) return null
+ val params = javax.servlet.http.HttpUtils.parseQueryString(queryString)
+ val keySet = params.keySet
+ keySet.forEach(key => {
+ if (key.equalsIgnoreCase("doAs")) return params.get(key)(0)
+ })
+
+ null
+ }
+}
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/CookieSigner.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/CookieSigner.scala
new file mode 100644
index 000000000..0c4bca1a7
--- /dev/null
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/CookieSigner.scala
@@ -0,0 +1,89 @@
+/*
+ * 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.kyuubi.server.http.util
+
+import java.security.{MessageDigest, NoSuchAlgorithmException}
+
+import org.apache.commons.codec.binary.Base64
+
+import org.apache.kyuubi.Logging
+
+object CookieSigner {
+ private val SIGNATURE = "&s="
+ private val SHA_STRING = "SHA-256"
+}
+
+class CookieSigner(secret: Array[Byte]) extends Logging {
+ private val secretBytes = secret.clone()
+
+ /**
+ * Sign the cookie given the string token as input.
+ *
+ * @param str Input token
+ * @return Signed token that can be used to create a cookie
+ */
+ def signCookie(str: String): String = {
+ if (str == null || str.isEmpty) {
+ throw new IllegalArgumentException("NULL or empty string to sign")
+ }
+ val signature = getSignature(str)
+ debug("Signature generated for " + str + " is " + signature)
+ str + CookieSigner.SIGNATURE + signature
+ }
+
+ /**
+ * Verify a signed string and extracts the original string.
+ *
+ * @param signedStr The already signed string
+ * @return Raw Value of the string without the signature
+ */
+ def verifyAndExtract(signedStr: String): String = {
+ val index = signedStr.lastIndexOf(CookieSigner.SIGNATURE)
+ if (index == -1) throw new IllegalArgumentException("Invalid input sign: " + signedStr)
+ val originalSignature = signedStr.substring(index + CookieSigner.SIGNATURE.length)
+ val rawValue = signedStr.substring(0, index)
+ val currentSignature = getSignature(rawValue)
+ debug("Signature generated for " + rawValue + " inside verify is " + currentSignature)
+ if (!MessageDigest.isEqual(originalSignature.getBytes, currentSignature.getBytes)) {
+ throw new IllegalArgumentException("Invalid sign, original = " + originalSignature +
+ " current = " + currentSignature)
+ }
+ rawValue
+ }
+
+ /**
+ * Get the signature of the input string based on SHA digest algorithm.
+ *
+ * @param str Input token
+ * @return Signed String
+ */
+ private def getSignature(str: String) =
+ try {
+ val md = MessageDigest.getInstance(CookieSigner.SHA_STRING)
+ md.update(str.getBytes)
+ md.update(secretBytes)
+ val digest = md.digest
+ new Base64(0).encodeToString(digest)
+ } catch {
+ case ex: NoSuchAlgorithmException =>
+ throw new RuntimeException(
+ "Invalid SHA digest String: " +
+ CookieSigner.SHA_STRING + " " + ex.getMessage,
+ ex)
+ }
+}
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/HttpAuthUtils.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/HttpAuthUtils.scala
new file mode 100644
index 000000000..7bb117476
--- /dev/null
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/HttpAuthUtils.scala
@@ -0,0 +1,97 @@
+/*
+ * 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.kyuubi.server.http.util
+
+import java.security.SecureRandom
+import java.util
+import java.util.StringTokenizer
+
+import scala.collection.mutable
+
+import org.apache.kyuubi.Logging
+
+object HttpAuthUtils extends Logging {
+ val WWW_AUTHENTICATE = "WWW-Authenticate"
+ val AUTHORIZATION = "Authorization"
+ val BASIC = "Basic"
+ val NEGOTIATE = "Negotiate"
+
+ private val COOKIE_ATTR_SEPARATOR = "&"
+ private val COOKIE_CLIENT_USER_NAME = "cu"
+ private val COOKIE_CLIENT_RAND_NUMBER = "rn"
+ private val COOKIE_KEY_VALUE_SEPARATOR = "="
+ private val COOKIE_ATTRIBUTES = new util.HashSet[String](
+ util.Arrays.asList(COOKIE_CLIENT_USER_NAME, COOKIE_CLIENT_RAND_NUMBER))
+
+ /**
+ * Creates and returns a HS2 cookie token.
+ *
+ * @param clientUserName Client User name.
+ * @return An unsigned cookie token generated from input parameters.
+ * The final cookie generated is of the following format :
+ * cu=<username>&rn=<randomNumber>&s=<cookieSignature>
+ */
+ def createCookieToken(clientUserName: String): String = {
+ val sb = new mutable.StringBuilder
+ sb.append(COOKIE_CLIENT_USER_NAME).append(COOKIE_KEY_VALUE_SEPARATOR)
+ .append(clientUserName).append(COOKIE_ATTR_SEPARATOR).append(COOKIE_CLIENT_RAND_NUMBER)
+ .append(COOKIE_KEY_VALUE_SEPARATOR).append((new SecureRandom).nextLong)
+ sb.toString
+ }
+
+ /**
+ * Parses a cookie token to retrieve client user name.
+ *
+ * @param tokenStr Token String.
+ * @return A valid user name if input is of valid format, else returns null.
+ */
+ def getUserNameFromCookieToken(tokenStr: String): String = {
+ val map = splitCookieToken(tokenStr)
+ if (!(map.keySet == COOKIE_ATTRIBUTES)) {
+ error("Invalid token with missing attributes " + tokenStr)
+ return null
+ }
+ map.get(COOKIE_CLIENT_USER_NAME)
+ }
+
+ /**
+ * Splits the cookie token into attributes pairs.
+ *
+ * @param tokenStr input token.
+ * @return a map with the attribute pairs of the token if the input is valid.
+ * Else, returns null.
+ */
+ private def splitCookieToken(tokenStr: String): util.Map[String, String] = {
+ val map = new util.HashMap[String, String]
+ val st = new StringTokenizer(tokenStr, COOKIE_ATTR_SEPARATOR)
+ while ({
+ st.hasMoreTokens
+ }) {
+ val part = st.nextToken
+ val separator = part.indexOf(COOKIE_KEY_VALUE_SEPARATOR)
+ if (separator == -1) {
+ error("Invalid token string " + tokenStr)
+ return null
+ }
+ val key = part.substring(0, separator)
+ val value = part.substring(separator + 1)
+ map.put(key, value)
+ }
+ map
+ }
+}
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/SessionManager.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/SessionManager.scala
new file mode 100644
index 000000000..f5c44411c
--- /dev/null
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/util/SessionManager.scala
@@ -0,0 +1,89 @@
+/*
+ * 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.kyuubi.server.http.util
+
+import org.apache.kyuubi.Logging
+
+object SessionManager extends Logging {
+
+ private val threadLocalIpAddress: ThreadLocal[String] = new ThreadLocal[String]
+
+ def setIpAddress(ipAddress: String): Unit = {
+ threadLocalIpAddress.set(ipAddress)
+ }
+
+ def clearIpAddress(): Unit = {
+ threadLocalIpAddress.remove()
+ }
+
+ def getIpAddress: String = {
+ threadLocalIpAddress.get
+ }
+
+ private val threadLocalForwardedAddresses: ThreadLocal[List[String]] =
+ new ThreadLocal[List[String]]
+
+ def setForwardedAddresses(ipAddress: List[String]): Unit = {
+ threadLocalForwardedAddresses.set(ipAddress)
+ }
+
+ def clearForwardedAddresses(): Unit = {
+ threadLocalForwardedAddresses.remove()
+ }
+
+ def getForwardedAddresses: List[String] = {
+ threadLocalForwardedAddresses.get
+ }
+
+ private val threadLocalUserName: ThreadLocal[String] = new ThreadLocal[String]() {
+ override protected def initialValue: String = {
+ null
+ }
+ }
+
+ def setUserName(userName: String): Unit = {
+ threadLocalUserName.set(userName)
+ }
+
+ def clearUserName(): Unit = {
+ threadLocalUserName.remove()
+ }
+
+ def getUserName: String = {
+ threadLocalUserName.get
+ }
+
+ private val threadLocalProxyUserName: ThreadLocal[String] = new ThreadLocal[String]() {
+ override protected def initialValue: String = {
+ null
+ }
+ }
+
+ def setProxyUserName(userName: String): Unit = {
+ debug("setting proxy user name based on query param to: " + userName)
+ threadLocalProxyUserName.set(userName)
+ }
+
+ def getProxyUserName: String = {
+ threadLocalProxyUserName.get
+ }
+
+ def clearProxyUserName(): Unit = {
+ threadLocalProxyUserName.remove()
+ }
+}
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/thrift/http/KyuubiOperationThriftHttpKerberosAndPlainAuthSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/thrift/http/KyuubiOperationThriftHttpKerberosAndPlainAuthSuite.scala
new file mode 100644
index 000000000..9d189b565
--- /dev/null
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/thrift/http/KyuubiOperationThriftHttpKerberosAndPlainAuthSuite.scala
@@ -0,0 +1,42 @@
+/*
+ * 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.kyuubi.operation.thrift.http
+
+import org.scalactic.source.Position
+import org.scalatest.Tag
+
+import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols
+import org.apache.kyuubi.operation.KyuubiOperationKerberosAndPlainAuthSuite
+
+class KyuubiOperationThriftHttpKerberosAndPlainAuthSuite
+ extends KyuubiOperationKerberosAndPlainAuthSuite {
+ override protected val frontendProtocols: Seq[KyuubiConf.FrontendProtocols.Value] =
+ FrontendProtocols.THRIFT_HTTP :: Nil
+
+ override protected def getJdbcUrl: String =
+ s"jdbc:hive2://${server.frontendServices.head.connectionUrl}/default;transportMode=http;" +
+ s"httpPath=cliservice"
+
+ override protected def test(testName: String, testTags: Tag*)(testFun: => Any)(implicit
+ pos: Position): Unit = {
+ if (!testName.equals("test with KERBEROS authentication")) {
+ super.test(testName, testTags: _*)(testFun)(pos)
+ }
+ }
+}
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/thrift/http/KyuubiOperationThriftHttpPerUserSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/thrift/http/KyuubiOperationThriftHttpPerUserSuite.scala
new file mode 100644
index 000000000..8a09d8c43
--- /dev/null
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/thrift/http/KyuubiOperationThriftHttpPerUserSuite.scala
@@ -0,0 +1,33 @@
+/*
+ * 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.kyuubi.operation.thrift.http
+
+import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols
+import org.apache.kyuubi.operation.KyuubiOperationPerUserSuite
+
+class KyuubiOperationThriftHttpPerUserSuite extends KyuubiOperationPerUserSuite {
+ override protected val frontendProtocols: Seq[KyuubiConf.FrontendProtocols.Value] =
+ FrontendProtocols.THRIFT_HTTP :: Nil
+
+ override protected def getJdbcUrl: String =
+ s"jdbc:hive2://${server.frontendServices.head.connectionUrl}/;transportMode=http;" +
+ s"httpPath=cliservice;"
+
+ override protected lazy val httpMode = true;
+}