diff --git a/docs/deployment/settings.md b/docs/deployment/settings.md
index cdb5451e4..977a878b4 100644
--- a/docs/deployment/settings.md
+++ b/docs/deployment/settings.md
@@ -184,7 +184,7 @@ kyuubi\.engine
\.operation\.log\.dir
\.root|
-1
|The size of engine pool. Note that, if the size is less than 1, the engine pool will not be enabled; otherwise, the size of the engine pool will be min(this, kyuubi.engine.pool.size.threshold).
|int
|1.4.0
kyuubi\.engine\.pool
\.size\.threshold|9
|This parameter is introduced as a server-side parameter, and controls the upper limit of the engine pool.
|int
|1.4.0
kyuubi\.engine\.session
\.initialize\.sql|SHOW DATABASES
|SemiColon-separated list of SQL statements to be initialized in the newly created engine session before queries. This configuration can not be used in JDBC url due to the limitation of Beeline/JDBC driver.
|seq
|1.3.0
-kyuubi\.engine\.share
\.level|USER
|Engines will be shared in different levels, available configs are:
- CONNECTION: engine will not be shared but only used by the current client connection
- USER: engine will be shared by all sessions created by a unique username, see also kyuubi.engine.share.level.subdomain
- SERVER: the App will be shared by Kyuubi servers
|string
|1.2.0
+kyuubi\.engine\.share
\.level|USER
|Engines will be shared in different levels, available configs are:
- CONNECTION: engine will not be shared but only used by the current client connection
- USER: engine will be shared by all sessions created by a unique username, see also kyuubi.engine.share.level.subdomain
- GROUP: engine will be shared by all sessions created by all users belong to the same primary group name. The engine will be launched by the group name as the effective username, so here the group name is kind of special user who is able to visit the compute resources/data of a team. It follows the [Hadoop GroupsMapping](https://reurl.cc/xE61Y5) to map user to a primary group. If the primary group is not found, it fallback to the USER level.
- SERVER: the App will be shared by Kyuubi servers
|string
|1.2.0
kyuubi\.engine\.share
\.level\.sub\.domain|<undefined>
|(deprecated) - Using kyuubi.engine.share.level.subdomain instead
|string
|1.2.0
kyuubi\.engine\.share
\.level\.subdomain|<undefined>
|Allow end-users to create a subdomain for the share level of an engine. A subdomain is a case-insensitive string values in `^[a-zA-Z_-]{1,14}$` form. For example, for `USER` share level, an end-user can share a certain engine within a subdomain, not for all of its clients. End-users are free to create multiple engines in the `USER` share level
|string
|1.4.0
kyuubi\.engine\.single
\.spark\.session|false
|When set to true, this engine is running in a single session mode. All the JDBC/ODBC connections share the temporary views, function registries, SQL configuration and the current database.
|boolean
|1.3.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 9e010b8d4..ba071ff0b 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
@@ -740,6 +740,12 @@ object KyuubiConf {
" connection" +
" USER: engine will be shared by all sessions created by a unique username," +
s" see also ${ENGINE_SHARE_LEVEL_SUBDOMAIN.key}" +
+ " GROUP: engine will be shared by all sessions created by all users belong to the same" +
+ " primary group name. The engine will be launched by the group name as the effective" +
+ " username, so here the group name is kind of special user who is able to visit the" +
+ " compute resources/data of a team. It follows the" +
+ " [Hadoop GroupsMapping](https://reurl.cc/xE61Y5) to map user to a primary group. If the" +
+ " primary group is not found, it fallback to the USER level." +
" SERVER: the App will be shared by Kyuubi servers")
.version("1.2.0")
.fallbackConf(LEGACY_ENGINE_SHARE_LEVEL)
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/ShareLevel.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/ShareLevel.scala
index 8222c6cd0..efa003eb2 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/ShareLevel.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/engine/ShareLevel.scala
@@ -27,6 +27,8 @@ object ShareLevel extends Enumeration {
CONNECTION,
/** DEFAULT level, An APP will be shared for all sessions created by a user */
USER,
+ /** In this level, An APP will be shared for all sessions created by a user's default group */
+ GROUP,
/** In this level, All sessions from one or more Kyuubi server's will share one single APP */
SERVER = Value
}
diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/JDBCTestUtils.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/JDBCTestUtils.scala
index 406606b5a..1decd7bc6 100644
--- a/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/JDBCTestUtils.scala
+++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/operation/JDBCTestUtils.scala
@@ -30,7 +30,7 @@ import org.apache.kyuubi.service.authentication.PlainSASLHelper
trait JDBCTestUtils extends KyuubiFunSuite {
protected val dftSchema = "default"
- protected val user: String = Utils.currentUser
+ protected lazy val user: String = Utils.currentUser
protected val patterns = Seq("", "*", "%", null, ".*", "_*", "_%", ".%")
protected def jdbcUrl: String
private var _sessionConfs: Map[String, String] = Map.empty
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala
index 8abcdaf71..146a06eb9 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/engine/EngineRef.scala
@@ -27,11 +27,12 @@ import com.google.common.annotations.VisibleForTesting
import org.apache.curator.framework.CuratorFramework
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex
import org.apache.curator.utils.ZKPaths
+import org.apache.hadoop.security.UserGroupInformation
import org.apache.kyuubi.{KyuubiSQLException, Logging, Utils}
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.config.KyuubiConf._
-import org.apache.kyuubi.engine.ShareLevel.{CONNECTION, SERVER, ShareLevel}
+import org.apache.kyuubi.engine.ShareLevel.{CONNECTION, GROUP, SERVER, ShareLevel}
import org.apache.kyuubi.engine.spark.SparkProcessBuilder
import org.apache.kyuubi.ha.HighAvailabilityConf.HA_ZK_ENGINE_REF_ID
import org.apache.kyuubi.ha.HighAvailabilityConf.HA_ZK_NAMESPACE
@@ -87,6 +88,16 @@ private[kyuubi] class EngineRef(
// Launcher of the engine
private[kyuubi] val appUser: String = shareLevel match {
case SERVER => Utils.currentUser
+ case GROUP =>
+ val clientUGI = UserGroupInformation.createRemoteUser(user)
+ // Similar to `clientUGI.getPrimaryGroupName` (avoid IOE) to get the Primary GroupName of
+ // the client user mapping to
+ clientUGI.getGroupNames.headOption match {
+ case Some(primaryGroup) => primaryGroup
+ case None =>
+ warn(s"There is no primary group for $user, use the client user name as group directly")
+ user
+ }
case _ => user
}
@@ -109,7 +120,10 @@ private[kyuubi] class EngineRef(
* /`serverSpace_CONNECTION`/`user`/`engineRefId`
* For `USER` share level:
* /`serverSpace_USER`/`user`[/`subdomain`]
- *
+ * For `GROUP` share level:
+ * /`serverSpace_GROUP`/`primary group name`[/`subdomain`]
+ * For `SERVER` share level:
+ * /`serverSpace_SERVER`/`kyuubi server user`[/`subdomain`]
*/
@VisibleForTesting
private[kyuubi] lazy val engineSpace: String = shareLevel match {
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/credentials/HiveDelegationTokenProviderSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/credentials/HiveDelegationTokenProviderSuite.scala
index 5f811fa29..e847e9119 100644
--- a/kyuubi-server/src/test/scala/org/apache/kyuubi/credentials/HiveDelegationTokenProviderSuite.scala
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/credentials/HiveDelegationTokenProviderSuite.scala
@@ -34,6 +34,7 @@ import org.apache.hadoop.hive.metastore.{HiveMetaException, HiveMetaStore}
import org.apache.hadoop.hive.thrift.{DelegationTokenIdentifier, HadoopThriftAuthBridge, HadoopThriftAuthBridge23}
import org.apache.hadoop.io.Text
import org.apache.hadoop.security.{Credentials, UserGroupInformation}
+import org.apache.hadoop.security.authorize.ProxyUsers
import org.apache.thrift.TProcessor
import org.apache.thrift.protocol.TProtocol
import org.scalatest.concurrent.Eventually._
@@ -82,6 +83,7 @@ class HiveDelegationTokenProviderSuite extends KerberizedTestHelper {
hiveConf.setVar(METASTORE_USE_THRIFT_SASL, "true")
hiveConf.setVar(METASTORE_KERBEROS_PRINCIPAL, testPrincipal)
hiveConf.setVar(METASTORE_KERBEROS_KEYTAB_FILE, testKeytab)
+ ProxyUsers.refreshSuperUserGroupsConfiguration(hiveConf)
val metaServer = new LocalMetaServer(hiveConf, classloader)
metaServer.start()
}
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/EngineRefSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/EngineRefSuite.scala
index 54bf41d17..e06b79182 100644
--- a/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/EngineRefSuite.scala
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/engine/EngineRefSuite.scala
@@ -20,6 +20,7 @@ package org.apache.kyuubi.engine
import java.util.UUID
import org.apache.curator.utils.ZKPaths
+import org.apache.hadoop.security.UserGroupInformation
import org.scalatest.time.SpanSugar.convertIntToGrainOfTime
import org.apache.kyuubi.{KyuubiFunSuite, Utils}
@@ -34,7 +35,7 @@ class EngineRefSuite extends KyuubiFunSuite {
import ShareLevel._
private val zkServer = new EmbeddedZookeeper
private val conf = KyuubiConf()
- val user = Utils.currentUser
+ private val user = Utils.currentUser
override def beforeAll(): Unit = {
val zkData = Utils.createTempDir()
@@ -87,6 +88,32 @@ class EngineRefSuite extends KyuubiFunSuite {
}
}
+ test("GROUP shared level engine name") {
+ val id = UUID.randomUUID().toString
+ conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, GROUP.toString)
+ val engineRef = new EngineRef(conf, user, id)
+ val primaryGroupName = UserGroupInformation.createRemoteUser(user).getPrimaryGroupName
+ assert(engineRef.engineSpace === ZKPaths.makePath(s"kyuubi_GROUP", primaryGroupName))
+ assert(engineRef.defaultEngineName === s"kyuubi_GROUP_${primaryGroupName}_$id")
+
+ Seq(KyuubiConf.ENGINE_SHARE_LEVEL_SUBDOMAIN,
+ KyuubiConf.ENGINE_SHARE_LEVEL_SUB_DOMAIN).foreach { k =>
+ conf.unset(k)
+ conf.set(k.key, "abc")
+ val engineRef2 = new EngineRef(conf, user, id)
+ assert(engineRef2.engineSpace ===
+ ZKPaths.makePath(s"kyuubi_$GROUP", primaryGroupName, "abc"))
+ assert(engineRef2.defaultEngineName === s"kyuubi_${GROUP}_${primaryGroupName}_abc_$id")
+ }
+
+ val userName = "Iamauserwithoutgroup"
+ val newUGI = UserGroupInformation.createRemoteUser(userName)
+ assert(newUGI.getGroupNames.isEmpty)
+ val engineRef3 = new EngineRef(conf, userName, id)
+ assert(engineRef3.engineSpace === ZKPaths.makePath(s"kyuubi_GROUP", userName, "abc"))
+ assert(engineRef3.defaultEngineName === s"kyuubi_GROUP_${userName}_abc_$id")
+ }
+
test("SERVER shared level engine name") {
val id = UUID.randomUUID().toString
conf.set(KyuubiConf.ENGINE_SHARE_LEVEL, SERVER.toString)
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationGroupSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationGroupSuite.scala
new file mode 100644
index 000000000..652fe9ce9
--- /dev/null
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationGroupSuite.scala
@@ -0,0 +1,61 @@
+/*
+ * 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
+
+import org.apache.hadoop.security.UserGroupInformation
+
+import org.apache.kyuubi.WithKyuubiServer
+import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.util.KyuubiHadoopUtils
+
+class KyuubiOperationGroupSuite extends WithKyuubiServer with JDBCTests {
+
+ override protected def jdbcUrl: String = getJdbcUrl
+
+ override protected val conf: KyuubiConf = {
+ val c = KyuubiConf().set(KyuubiConf.ENGINE_SHARE_LEVEL, "group")
+ .set("hadoop.user.group.static.mapping.overrides",
+ s"user1=testGG,group_tt;user2=testGG")
+ UserGroupInformation.setConfiguration(KyuubiHadoopUtils.newHadoopConf(c))
+ c.set(s"hadoop.proxyuser.$user.groups", "*")
+ .set(s"hadoop.proxyuser.$user.hosts", "*")
+ }
+
+ test("ensure two connections in group mode share the same engine started by primary group") {
+ var r1: String = null
+ var r2: String = null
+ withSessionConf(Map("hive.server2.proxy.user" -> "user1"))(Map.empty)(Map.empty) {
+ withJdbcStatement() { statement =>
+ val res = statement.executeQuery("set spark.app.name")
+ assert(res.next())
+ r1 = res.getString("value")
+ }
+ }
+
+ withSessionConf(Map("hive.server2.proxy.user" -> "user2"))(Map.empty)(Map.empty) {
+ withJdbcStatement() { statement =>
+ val res = statement.executeQuery("set spark.app.name")
+ assert(res.next())
+ r2 = res.getString("value")
+ }
+ }
+ assert(r1 != null && r2 != null)
+ assert(r1 === r2)
+ assert(r1.startsWith(s"kyuubi_GROUP_testGG"))
+ }
+}