Skip to content

Support LDAPGroupProvider #4001

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .idea/icon.png
Binary file not shown.
34 changes: 2 additions & 32 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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: " +
"<li>hadoop: delegate the user group mapping to hadoop UserGroupInformation.</li>")
"<li>hadoop: delegate the user group mapping to hadoop UserGroupInformation.</li>" +
"<li>ldap: delegate the user group mapping to ldap.</li>")
.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.")
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}