diff --git a/.idea/icon.png b/.idea/icon.png
deleted file mode 100644
index f0090030d38..00000000000
Binary files a/.idea/icon.png and /dev/null differ
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 7405930199c..35eb1ddfbbc 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,36 +1,6 @@
-
-
-
-
-
+
-
+
\ No newline at end of file
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 65f03c6546a..0eac32c9f67 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
@@ -2371,15 +2371,75 @@ object KyuubiConf {
"and groups information for different users or session configs. This config value " +
"should be a subclass of `org.apache.kyuubi.plugin.GroupProvider` which " +
"has a zero-arg constructor. Kyuubi provides the following built-in implementations: " +
- "
hadoop: delegate the user group mapping to hadoop UserGroupInformation.
")
+ "
hadoop: delegate the user group mapping to hadoop UserGroupInformation.
" +
+ "
ldap: delegate the user group mapping to ldap.
")
.version("1.7.0")
.stringConf
.transform {
case "hadoop" => "org.apache.kyuubi.session.HadoopGroupProvider"
+ case "ldap" => "org.apache.kyuubi.session.LDAPGroupProvider"
case other => other
}
.createWithDefault("hadoop")
+ val LDAP_GROUP_PROVIDER_URL: OptionalConfigEntry[String] =
+ buildConf("kyuubi.session.group.ldap.url")
+ .doc("SPACE character separated LDAP connection URL(s).")
+ .version("1.7.0")
+ .stringConf
+ .createOptional
+
+ val LDAP_GROUP_PROVIDER_BIND_DN: OptionalConfigEntry[String] =
+ buildConf("kyuubi.session.group.ldap.bind.dn")
+ .doc("LDAP bind DN used to connect ldap server.")
+ .version("1.7.0")
+ .stringConf
+ .createOptional
+
+ val LDAP_GROUP_PROVIDER_BASED_DN: OptionalConfigEntry[String] =
+ buildConf("kyuubi.session.group.ldap.based.dn")
+ .doc("LDAP base DN.")
+ .version("1.7.0")
+ .stringConf
+ .createOptional
+
+ val LDAP_GROUP_PROVIDER_BIND_PASSWORD: OptionalConfigEntry[String] =
+ buildConf("kyuubi.session.group.ldap.bind.password")
+ .doc("LDAP bind password connect ldap server with bind DN.")
+ .version("1.7.0")
+ .stringConf
+ .createOptional
+
+ val LDAP_GROUP_PROVIDER_GROUP_MEMBER_ATTR: ConfigEntry[String] =
+ buildConf("kyuubi.session.group.ldap.group.member.attr")
+ .doc("LDAP group member attribute")
+ .version("1.7.0")
+ .stringConf
+ .createWithDefault("member")
+
+ val LDAP_GROUP_PROVIDER_GROUP_NAME_ATTR: ConfigEntry[String] =
+ buildConf("kyuubi.session.group.ldap.group.name.attr")
+ .doc("LDAP group name attribute")
+ .version("1.7.0")
+ .stringConf
+ .createWithDefault("cn")
+
+ val LDAP_GROUP_PROVIDER_GROUP_SEARCH_FILTER: ConfigEntry[String] =
+ buildConf("kyuubi.session.group.ldap.group.search.filter")
+ .doc("LDAP group search filter, if version of ldap > 2.0, it can use default value, else" +
+ " it may be set (objectClass=group)")
+ .version("1.7.0")
+ .stringConf
+ .createWithDefault("(objectClass=groupOfNames)")
+
+ val LDAP_GROUP_PROVIDER_USER_SEARCH_FILTER: ConfigEntry[String] =
+ buildConf("kyuubi.session.group.ldap.user.search.filter")
+ .doc("LDAP user search filter, if version of ldap > 2.0, it can use default value, else" +
+ " it may be set (&(objectClass=user)(sAMAccountName={0}))")
+ .version("1.7.0")
+ .stringConf
+ .createWithDefault("(&(objectClass=person)(cn={0}))")
+
val SERVER_NAME: OptionalConfigEntry[String] =
buildConf("kyuubi.server.name")
.doc("The name of Kyuubi Server.")
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/LDAPGroupProvider.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/LDAPGroupProvider.scala
new file mode 100644
index 00000000000..608353b633b
--- /dev/null
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/LDAPGroupProvider.scala
@@ -0,0 +1,118 @@
+/*
+ * 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.session
+
+import java.util.{Map => JMap}
+import javax.naming.{Context, NamingException}
+import javax.naming.directory.{InitialDirContext, SearchControls}
+import javax.security.sasl.AuthenticationException
+
+import scala.collection.mutable.ArrayBuffer
+
+import org.apache.kyuubi.Logging
+import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.config.KyuubiConf.{LDAP_GROUP_PROVIDER_BASED_DN, LDAP_GROUP_PROVIDER_BIND_DN, LDAP_GROUP_PROVIDER_BIND_PASSWORD, LDAP_GROUP_PROVIDER_GROUP_MEMBER_ATTR, LDAP_GROUP_PROVIDER_GROUP_NAME_ATTR, LDAP_GROUP_PROVIDER_GROUP_SEARCH_FILTER, LDAP_GROUP_PROVIDER_URL, LDAP_GROUP_PROVIDER_USER_SEARCH_FILTER}
+import org.apache.kyuubi.plugin.GroupProvider
+
+class LDAPGroupProvider extends GroupProvider with Logging {
+ private var ctx: InitialDirContext = _
+ private val serverConf: KyuubiConf = new KyuubiConf().loadFileDefaults()
+
+ private def initDirContext(): InitialDirContext = {
+ val bindDn = serverConf.get(LDAP_GROUP_PROVIDER_BIND_DN).get
+ val bindPw = serverConf.get(LDAP_GROUP_PROVIDER_BIND_PASSWORD).get
+ val env = new java.util.Hashtable[String, Any]()
+ env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
+ env.put(Context.SECURITY_AUTHENTICATION, "simple")
+
+ env.put(Context.SECURITY_PRINCIPAL, bindDn)
+ env.put(Context.SECURITY_CREDENTIALS, bindPw)
+ serverConf
+ .get(LDAP_GROUP_PROVIDER_URL)
+ .foreach(env.put(Context.PROVIDER_URL, _))
+ try {
+ ctx = new InitialDirContext(env)
+ } catch {
+ case e: NamingException =>
+ ctx = null
+ throw new AuthenticationException(
+ s"Error validating LDAP user: $bindDn",
+ e)
+ }
+ ctx
+ }
+
+ private def getDirContext(): InitialDirContext = {
+ if (ctx == null) {
+ synchronized {
+ if (ctx == null) {
+ ctx = initDirContext()
+ }
+ }
+ }
+ ctx
+ }
+
+ override def primaryGroup(user: String, sessionConf: JMap[String, String]): String =
+ groups(user, sessionConf).head
+
+ override def groups(user: String, sessionConf: JMap[String, String]): Array[String] = {
+ val userBasedDN = serverConf.get(LDAP_GROUP_PROVIDER_BASED_DN).get
+ val groupMemberAttr = serverConf.get(LDAP_GROUP_PROVIDER_GROUP_MEMBER_ATTR)
+ val groupNameAttr = serverConf.get(LDAP_GROUP_PROVIDER_GROUP_NAME_ATTR)
+ val groupSearchFilter = serverConf.get(LDAP_GROUP_PROVIDER_GROUP_SEARCH_FILTER)
+ val mGroupQuery = "(&%s(%s={0}))".format(groupSearchFilter, groupMemberAttr)
+ val userSearchFilter = serverConf.get(LDAP_GROUP_PROVIDER_USER_SEARCH_FILTER)
+ val mappingGroups = new ArrayBuffer[String]
+ val sc = new SearchControls
+ sc.setSearchScope(SearchControls.SUBTREE_SCOPE)
+
+ try {
+ val ctx = getDirContext()
+ val answers =
+ ctx.search(userBasedDN, userSearchFilter, Array[AnyRef](user), sc)
+
+ while (answers.hasMoreElements) {
+ val answer = answers.next()
+ val userDistinguishedName = answer.getNameInNamespace
+
+ val groupResults =
+ ctx.search(userBasedDN, mGroupQuery, Array[AnyRef](userDistinguishedName), sc)
+ if (groupResults != null) while ({
+ groupResults.hasMoreElements
+ }) {
+ val groupResult = groupResults.nextElement
+ val groupName = groupResult.getAttributes.get(groupNameAttr)
+ mappingGroups.append(groupName.get.toString)
+ }
+ }
+
+ } catch {
+ case e: NamingException =>
+ if (ctx != null) {
+ ctx.close()
+ ctx = null
+ }
+ throw new AuthenticationException(
+ s"Error search group for user: $user",
+ e)
+ }
+ info(s"mappingGroups $mappingGroups")
+ mappingGroups.toArray
+ }
+}