diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md
index 020cb0efdd5..b8f584d7bdf 100644
--- a/docs/configuration/settings.md
+++ b/docs/configuration/settings.md
@@ -31,32 +31,36 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co
### Authentication
-| Key | Default | Meaning | Type | Since |
-|-----------------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|--------|
-| kyuubi.authentication | NONE | A comma-separated list of client authentication types.
- NOSASL: raw transport.
- NONE: no authentication check.
- KERBEROS: Kerberos/GSSAPI authentication.
- CUSTOM: User-defined authentication.
- JDBC: JDBC query authentication.
- LDAP: Lightweight Directory Access Protocol authentication.
The following tree describes the catalog of each option. Note that: for SASL authentication, KERBEROS and PLAIN auth types are supported at the same time, and only the first specified PLAIN auth type is valid. | seq | 1.0.0 |
-| kyuubi.authentication.custom.basic.class | <undefined> | User-defined authentication implementation of org.apache.kyuubi.service.authentication.PasswdAuthenticationProvider for http basic authentication. | string | 1.10.0 |
-| kyuubi.authentication.custom.bearer.class | <undefined> | User-defined authentication implementation of org.apache.kyuubi.service.authentication.TokenAuthenticationProvider for http bearer authentication. | string | 1.10.0 |
-| kyuubi.authentication.custom.class | <undefined> | User-defined authentication implementation of org.apache.kyuubi.service.authentication.PasswdAuthenticationProvider | string | 1.3.0 |
-| kyuubi.authentication.jdbc.driver.class | <undefined> | Driver class name for JDBC Authentication Provider. | string | 1.6.0 |
-| kyuubi.authentication.jdbc.password | <undefined> | Database password for JDBC Authentication Provider. | string | 1.6.0 |
-| kyuubi.authentication.jdbc.query | <undefined> | Query SQL template with placeholders for JDBC Authentication Provider to execute. Authentication passes if the result set is not empty.The SQL statement must start with the `SELECT` clause. Available placeholders are `${user}` and `${password}`. | string | 1.6.0 |
-| kyuubi.authentication.jdbc.url | <undefined> | JDBC URL for JDBC Authentication Provider. | string | 1.6.0 |
-| kyuubi.authentication.jdbc.user | <undefined> | Database user for JDBC Authentication Provider. | string | 1.6.0 |
-| kyuubi.authentication.ldap.baseDN | <undefined> | LDAP base DN. | string | 1.7.0 |
-| kyuubi.authentication.ldap.binddn | <undefined> | The user with which to bind to the LDAP server, and search for the full domain name of the user being authenticated. This should be the full domain name of the user, and should have search access across all users in the LDAP tree. If not specified, then the user being authenticated will be used as the bind user. For example: CN=bindUser,CN=Users,DC=subdomain,DC=domain,DC=com | string | 1.7.0 |
-| kyuubi.authentication.ldap.bindpw | <undefined> | The password for the bind user, to be used to search for the full name of the user being authenticated. If the username is specified, this parameter must also be specified. | string | 1.7.0 |
-| kyuubi.authentication.ldap.customLDAPQuery | <undefined> | A full LDAP query that LDAP Atn provider uses to execute against LDAP Server. If this query returns a null resultset, the LDAP Provider fails the Authentication request, succeeds if the user is part of the resultset.For example: `(&(objectClass=group)(objectClass=top)(instanceType=4)(cn=Domain*))`, `(&(objectClass=person)(|(sAMAccountName=admin)(|(memberOf=CN=Domain Admins,CN=Users,DC=domain,DC=com)(memberOf=CN=Administrators,CN=Builtin,DC=domain,DC=com))))` | string | 1.7.0 |
-| kyuubi.authentication.ldap.domain | <undefined> | LDAP domain. | string | 1.0.0 |
-| kyuubi.authentication.ldap.groupClassKey | groupOfNames | LDAP attribute name on the group entry that is to be used in LDAP group searches. For example: group, groupOfNames or groupOfUniqueNames. | string | 1.7.0 |
-| kyuubi.authentication.ldap.groupDNPattern | <undefined> | COLON-separated list of patterns to use to find DNs for group entities in this directory. Use %s where the actual group name is to be substituted for. For example: CN=%s,CN=Groups,DC=subdomain,DC=domain,DC=com. | string | 1.7.0 |
-| kyuubi.authentication.ldap.groupFilter || COMMA-separated list of LDAP Group names (short name not full DNs). For example: HiveAdmins,HadoopAdmins,Administrators | set | 1.7.0 |
-| kyuubi.authentication.ldap.groupMembershipKey | member | LDAP attribute name on the group object that contains the list of distinguished names for the user, group, and contact objects that are members of the group. For example: member, uniqueMember or memberUid | string | 1.7.0 |
-| kyuubi.authentication.ldap.guidKey | uid | LDAP attribute name whose values are unique in this LDAP server. For example: uid or CN. | string | 1.2.0 |
-| kyuubi.authentication.ldap.url | <undefined> | SPACE character separated LDAP connection URL(s). | string | 1.0.0 |
-| kyuubi.authentication.ldap.userDNPattern | <undefined> | COLON-separated list of patterns to use to find DNs for users in this directory. Use %s where the actual group name is to be substituted for. For example: CN=%s,CN=Users,DC=subdomain,DC=domain,DC=com. | string | 1.7.0 |
-| kyuubi.authentication.ldap.userFilter || COMMA-separated list of LDAP usernames (just short names, not full DNs). For example: hiveuser,impalauser,hiveadmin,hadoopadmin | set | 1.7.0 |
-| kyuubi.authentication.ldap.userMembershipKey | <undefined> | LDAP attribute name on the user object that contains groups of which the user is a direct member, except for the primary group, which is represented by the primaryGroupId. For example: memberOf | string | 1.7.0 |
-| kyuubi.authentication.sasl.qop | auth | Sasl QOP enable higher levels of protection for Kyuubi communication with clients. - auth - authentication only (default)
- auth-int - authentication plus integrity protection
- auth-conf - authentication plus integrity and confidentiality protection. This is applicable only if Kyuubi is configured to use Kerberos authentication.
| string | 1.0.0 |
+| Key | Default | Meaning | Type | Since |
+|----------------------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|--------|
+| kyuubi.authentication | NONE | A comma-separated list of client authentication types. - NOSASL: raw transport.
- NONE: no authentication check.
- KERBEROS: Kerberos/GSSAPI authentication.
- CUSTOM: User-defined authentication.
- JDBC: JDBC query authentication.
- LDAP: Lightweight Directory Access Protocol authentication.
The following tree describes the catalog of each option. Note that: for SASL authentication, KERBEROS and PLAIN auth types are supported at the same time, and only the first specified PLAIN auth type is valid. | seq | 1.0.0 |
+| kyuubi.authentication.custom.basic.class | <undefined> | User-defined authentication implementation of org.apache.kyuubi.service.authentication.PasswdAuthenticationProvider for http basic authentication. | string | 1.10.0 |
+| kyuubi.authentication.custom.bearer.class | <undefined> | User-defined authentication implementation of org.apache.kyuubi.service.authentication.TokenAuthenticationProvider for http bearer authentication. | string | 1.10.0 |
+| kyuubi.authentication.custom.class | <undefined> | User-defined authentication implementation of org.apache.kyuubi.service.authentication.PasswdAuthenticationProvider | string | 1.3.0 |
+| kyuubi.authentication.jdbc.driver.class | <undefined> | Driver class name for JDBC Authentication Provider. | string | 1.6.0 |
+| kyuubi.authentication.jdbc.password | <undefined> | Database password for JDBC Authentication Provider. | string | 1.6.0 |
+| kyuubi.authentication.jdbc.query | <undefined> | Query SQL template with placeholders for JDBC Authentication Provider to execute. Authentication passes if the result set is not empty.The SQL statement must start with the `SELECT` clause. Available placeholders are `${user}` and `${password}`. | string | 1.6.0 |
+| kyuubi.authentication.jdbc.url | <undefined> | JDBC URL for JDBC Authentication Provider. | string | 1.6.0 |
+| kyuubi.authentication.jdbc.user | <undefined> | Database user for JDBC Authentication Provider. | string | 1.6.0 |
+| kyuubi.authentication.ldap.baseDN | <undefined> | LDAP base DN. | string | 1.7.0 |
+| kyuubi.authentication.ldap.binddn | <undefined> | The user with which to bind to the LDAP server, and search for the full domain name of the user being authenticated. This should be the full domain name of the user, and should have search access across all users in the LDAP tree. If not specified, then the user being authenticated will be used as the bind user. For example: CN=bindUser,CN=Users,DC=subdomain,DC=domain,DC=com | string | 1.7.0 |
+| kyuubi.authentication.ldap.bindpw | <undefined> | The password for the bind user, to be used to search for the full name of the user being authenticated. If the username is specified, this parameter must also be specified. | string | 1.7.0 |
+| kyuubi.authentication.ldap.customLDAPQuery | <undefined> | A full LDAP query that LDAP Atn provider uses to execute against LDAP Server. If this query returns a null resultset, the LDAP Provider fails the Authentication request, succeeds if the user is part of the resultset.For example: `(&(objectClass=group)(objectClass=top)(instanceType=4)(cn=Domain*))`, `(&(objectClass=person)(|(sAMAccountName=admin)(|(memberOf=CN=Domain Admins,CN=Users,DC=domain,DC=com)(memberOf=CN=Administrators,CN=Builtin,DC=domain,DC=com))))` | string | 1.7.0 |
+| kyuubi.authentication.ldap.domain | <undefined> | LDAP domain. | string | 1.0.0 |
+| kyuubi.authentication.ldap.groupClassKey | groupOfNames | LDAP attribute name on the group entry that is to be used in LDAP group searches. For example: group, groupOfNames or groupOfUniqueNames. | string | 1.7.0 |
+| kyuubi.authentication.ldap.groupDNPattern | <undefined> | COLON-separated list of patterns to use to find DNs for group entities in this directory. Use %s where the actual group name is to be substituted for. For example: CN=%s,CN=Groups,DC=subdomain,DC=domain,DC=com. | string | 1.7.0 |
+| kyuubi.authentication.ldap.groupFilter || COMMA-separated list of LDAP Group names (short name not full DNs). For example: HiveAdmins,HadoopAdmins,Administrators | set | 1.7.0 |
+| kyuubi.authentication.ldap.groupMembershipKey | member | LDAP attribute name on the group object that contains the list of distinguished names for the user, group, and contact objects that are members of the group. For example: member, uniqueMember or memberUid | string | 1.7.0 |
+| kyuubi.authentication.ldap.guidKey | uid | LDAP attribute name whose values are unique in this LDAP server. For example: uid or CN. | string | 1.2.0 |
+| kyuubi.authentication.ldap.ssl.enable | false | Set this to true for using SSL encryption when connecting to LDAP servers. | boolean | 1.13.0 |
+| kyuubi.authentication.ldap.ssl.truststore.password | <undefined> | The truststore password used for SSL connections to LDAP servers. | string | 1.13.0 |
+| kyuubi.authentication.ldap.ssl.truststore.path | <undefined> | The truststore path used for SSL connections to LDAP servers. | string | 1.13.0 |
+| kyuubi.authentication.ldap.ssl.truststore.type | <undefined> | The truststore type used for SSL connections to LDAP servers. | string | 1.13.0 |
+| kyuubi.authentication.ldap.url | <undefined> | SPACE character separated LDAP connection URL(s). | string | 1.0.0 |
+| kyuubi.authentication.ldap.userDNPattern | <undefined> | COLON-separated list of patterns to use to find DNs for users in this directory. Use %s where the actual group name is to be substituted for. For example: CN=%s,CN=Users,DC=subdomain,DC=domain,DC=com. | string | 1.7.0 |
+| kyuubi.authentication.ldap.userFilter || COMMA-separated list of LDAP usernames (just short names, not full DNs). For example: hiveuser,impalauser,hiveadmin,hadoopadmin | set | 1.7.0 |
+| kyuubi.authentication.ldap.userMembershipKey | <undefined> | LDAP attribute name on the user object that contains groups of which the user is a direct member, except for the primary group, which is represented by the primaryGroupId. For example: memberOf | string | 1.7.0 |
+| kyuubi.authentication.sasl.qop | auth | Sasl QOP enable higher levels of protection for Kyuubi communication with clients. - auth - authentication only (default)
- auth-int - authentication plus integrity protection
- auth-conf - authentication plus integrity and confidentiality protection. This is applicable only if Kyuubi is configured to use Kerberos authentication.
| string | 1.0.0 |
### Backend
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 4239f8b6187..f28ca98f3cd 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
@@ -1230,6 +1230,42 @@ object KyuubiConf {
.stringConf
.createOptional
+ val AUTHENTICATION_LDAP_SSL_ENABLE: ConfigEntry[Boolean] =
+ buildConf("kyuubi.authentication.ldap.ssl.enable")
+ .doc("Set this to true for using SSL encryption when connecting to LDAP servers.")
+ .version("1.13.0")
+ .audience(SERVER)
+ .immutable
+ .booleanConf
+ .createWithDefault(false)
+
+ val AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PATH: OptionalConfigEntry[String] =
+ buildConf("kyuubi.authentication.ldap.ssl.truststore.path")
+ .doc("The truststore path used for SSL connections to LDAP servers.")
+ .version("1.13.0")
+ .audience(SERVER)
+ .immutable
+ .stringConf
+ .createOptional
+
+ val AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PASSWORD: OptionalConfigEntry[String] =
+ buildConf("kyuubi.authentication.ldap.ssl.truststore.password")
+ .doc("The truststore password used for SSL connections to LDAP servers.")
+ .version("1.13.0")
+ .audience(SERVER)
+ .immutable
+ .stringConf
+ .createOptional
+
+ val AUTHENTICATION_LDAP_SSL_TRUSTSTORE_TYPE: OptionalConfigEntry[String] =
+ buildConf("kyuubi.authentication.ldap.ssl.truststore.type")
+ .doc("The truststore type used for SSL connections to LDAP servers.")
+ .version("1.13.0")
+ .audience(SERVER)
+ .immutable
+ .stringConf
+ .createOptional
+
val AUTHENTICATION_JDBC_DRIVER: OptionalConfigEntry[String] =
buildConf("kyuubi.authentication.jdbc.driver.class")
.doc("Driver class name for JDBC Authentication Provider.")
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSSLSocketFactory.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSSLSocketFactory.scala
new file mode 100644
index 00000000000..24dcef0c452
--- /dev/null
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSSLSocketFactory.scala
@@ -0,0 +1,76 @@
+/*
+ * 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.service.authentication.ldap
+
+import java.io.IOException
+import java.net.{InetAddress, Socket}
+import javax.net.SocketFactory
+import javax.net.ssl.SSLContext
+
+final class LdapSSLSocketFactory private (socketFactory: SocketFactory) extends SocketFactory {
+
+ @throws[IOException]
+ override def createSocket(): Socket = socketFactory.createSocket()
+
+ @throws[IOException]
+ override def createSocket(host: String, port: Int): Socket =
+ socketFactory.createSocket(host, port)
+
+ @throws[IOException]
+ override def createSocket(
+ host: String,
+ port: Int,
+ localHost: InetAddress,
+ localPort: Int): Socket = {
+ socketFactory.createSocket(host, port, localHost, localPort)
+ }
+
+ @throws[IOException]
+ override def createSocket(host: InetAddress, port: Int): Socket =
+ socketFactory.createSocket(host, port)
+
+ @throws[IOException]
+ override def createSocket(
+ address: InetAddress,
+ port: Int,
+ localAddress: InetAddress,
+ localPort: Int): Socket = {
+ socketFactory.createSocket(address, port, localAddress, localPort)
+ }
+}
+
+object LdapSSLSocketFactory {
+
+ private val sslContext = new ThreadLocal[SSLContext]
+
+ def getDefault(): SocketFactory = {
+ val context = sslContext.get()
+ if (context == null) {
+ throw new IllegalStateException("SSLContext was not set for LDAP SSL connection")
+ }
+ new LdapSSLSocketFactory(context.getSocketFactory)
+ }
+
+ def setSSLContextForCurrentThread(context: SSLContext): Unit = {
+ sslContext.set(context)
+ }
+
+ def clearSslContextForCurrentThread(): Unit = {
+ sslContext.remove()
+ }
+}
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSSLUtils.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSSLUtils.scala
new file mode 100644
index 00000000000..6dc039ecf90
--- /dev/null
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSSLUtils.scala
@@ -0,0 +1,119 @@
+/*
+ * 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.service.authentication.ldap
+
+import java.io.{FileInputStream, IOException}
+import java.security.{GeneralSecurityException, KeyStore}
+import java.security.cert.{CertificateFactory, X509Certificate}
+import javax.net.ssl.{SSLContext, TrustManagerFactory}
+
+import scala.collection.JavaConverters._
+
+private[ldap] object LdapSSLUtils {
+
+ @throws[GeneralSecurityException]
+ @throws[IOException]
+ def createSSLContext(
+ trustStorePath: String,
+ trustStorePassword: String,
+ trustStoreType: String): SSLContext = {
+ val trustStore =
+ if (Option(trustStorePath).exists(_.trim.nonEmpty)) {
+ loadTrustStore(trustStorePath, trustStorePassword, trustStoreType)
+ } else {
+ null
+ }
+
+ val trustManagerFactory =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
+ trustManagerFactory.init(trustStore)
+
+ val sslContext = SSLContext.getInstance("SSL")
+ sslContext.init(null, trustManagerFactory.getTrustManagers, null)
+ sslContext
+ }
+
+ @throws[GeneralSecurityException]
+ @throws[IOException]
+ private def loadTrustStore(
+ trustStorePath: String,
+ trustStorePassword: String,
+ trustStoreType: String): KeyStore = {
+ val certificatesKeyStore =
+ try {
+ loadCertificates(trustStorePath)
+ } catch {
+ case _: GeneralSecurityException | _: IOException => None
+ }
+ certificatesKeyStore
+ .getOrElse(loadKeyStore(trustStorePath, trustStorePassword, trustStoreType))
+ }
+
+ @throws[GeneralSecurityException]
+ @throws[IOException]
+ private def loadKeyStore(
+ trustStorePath: String,
+ trustStorePassword: String,
+ trustStoreType: String): KeyStore = {
+ val trustStore =
+ KeyStore.getInstance(
+ Option(trustStoreType)
+ .filter(_.trim.nonEmpty)
+ .getOrElse(KeyStore.getDefaultType))
+ val in = new FileInputStream(trustStorePath)
+ try {
+ trustStore.load(in, toCharArray(trustStorePassword))
+ } finally {
+ in.close()
+ }
+ trustStore
+ }
+
+ @throws[GeneralSecurityException]
+ @throws[IOException]
+ private def loadCertificates(trustStorePath: String): Option[KeyStore] = {
+ val certificateFactory = CertificateFactory.getInstance("X.509")
+ val certificateChain = {
+ val in = new FileInputStream(trustStorePath)
+ try {
+ val certificates = certificateFactory.generateCertificates(in)
+ certificates.asScala.map(_.asInstanceOf[X509Certificate]).toSeq
+ } finally {
+ in.close()
+ }
+ }
+
+ if (certificateChain.isEmpty) {
+ None
+ } else {
+ val trustStore = KeyStore.getInstance(KeyStore.getDefaultType)
+ trustStore.load(null, null)
+ var index = 1
+ certificateChain.foreach { certificate =>
+ val certificateAlias = s"Certificate_$index";
+ trustStore.setCertificateEntry(certificateAlias, certificate)
+ index += 1
+ }
+ Some(trustStore)
+ }
+ }
+
+ private def toCharArray(password: String): Array[Char] = {
+ if (password == null) null else password.toCharArray
+ }
+}
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearch.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearch.scala
index 09dca1d5c3a..84bf31c8d3b 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearch.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearch.scala
@@ -30,7 +30,10 @@ import org.apache.kyuubi.config.KyuubiConf
* @param conf Kyuubi configuration
* @param ctx Directory service that will be used for the queries.
*/
-class LdapSearch(conf: KyuubiConf, ctx: DirContext) extends DirSearch with Logging {
+class LdapSearch(
+ conf: KyuubiConf,
+ ctx: DirContext,
+ clearSslContextOnClose: Boolean = false) extends DirSearch with Logging {
final private val baseDn = conf.get(KyuubiConf.AUTHENTICATION_LDAP_BASE_DN).orNull
final private val groupBases: Array[String] =
@@ -51,6 +54,10 @@ class LdapSearch(conf: KyuubiConf, ctx: DirContext) extends DirSearch with Loggi
catch {
case e: NamingException =>
warn("Exception when closing LDAP context:", e)
+ } finally {
+ if (clearSslContextOnClose) {
+ LdapSSLSocketFactory.clearSslContextForCurrentThread()
+ }
}
}
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearchFactory.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearchFactory.scala
index e3649d359e7..9c3287d8de3 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearchFactory.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearchFactory.scala
@@ -17,20 +17,24 @@
package org.apache.kyuubi.service.authentication.ldap
+import java.io.IOException
+import java.security.{GeneralSecurityException, KeyStore}
import java.util
import javax.naming.{Context, NamingException}
import javax.naming.directory.{DirContext, InitialDirContext}
+import javax.net.ssl.SSLContext
import javax.security.sasl.AuthenticationException
import org.apache.kyuubi.Logging
import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.config.KyuubiConf._
class LdapSearchFactory extends DirSearchFactory with Logging {
@throws[AuthenticationException]
override def getInstance(conf: KyuubiConf, principal: String, password: String): DirSearch = {
try {
val ctx = createDirContext(conf, principal, password)
- new LdapSearch(conf, ctx)
+ new LdapSearch(conf, ctx, conf.get(AUTHENTICATION_LDAP_SSL_ENABLE))
} catch {
case e: NamingException =>
debug(s"Could not connect to the LDAP Server: Authentication failed for $principal")
@@ -43,6 +47,22 @@ class LdapSearchFactory extends DirSearchFactory with Logging {
conf: KyuubiConf,
principal: String,
password: String): DirContext = {
+ val env = createDirContextEnvironment(conf, principal, password)
+ try {
+ new InitialDirContext(env)
+ } catch {
+ case e: NamingException =>
+ if (env.containsKey(LdapSearchFactory.LDAP_SOCKET_FACTORY)) {
+ LdapSSLSocketFactory.clearSslContextForCurrentThread()
+ }
+ throw e
+ }
+ }
+
+ private[ldap] def createDirContextEnvironment(
+ conf: KyuubiConf,
+ principal: String,
+ password: String): util.Hashtable[String, AnyRef] = {
val ldapUrl = conf.get(KyuubiConf.AUTHENTICATION_LDAP_URL)
val env = new util.Hashtable[String, AnyRef]
ldapUrl.foreach(env.put(Context.PROVIDER_URL, _))
@@ -50,7 +70,56 @@ class LdapSearchFactory extends DirSearchFactory with Logging {
env.put(Context.SECURITY_AUTHENTICATION, "simple")
env.put(Context.SECURITY_PRINCIPAL, principal)
env.put(Context.SECURITY_CREDENTIALS, password)
+ if (conf.get(AUTHENTICATION_LDAP_SSL_ENABLE)) {
+ configureSSLSocketFactory(conf, env)
+ }
debug(s"Connecting using principal $principal to ldap server: ${ldapUrl.orNull}")
- new InitialDirContext(env)
+ env
+ }
+
+ @throws[NamingException]
+ private def configureSSLSocketFactory(
+ conf: KyuubiConf,
+ env: util.Hashtable[String, AnyRef]): Unit = {
+ val trustStorePath = conf.get(AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PATH)
+ val trustStorePassword = conf.get(AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PASSWORD)
+ val trustStoreType = conf.get(AUTHENTICATION_LDAP_SSL_TRUSTSTORE_TYPE)
+
+ if (trustStorePath.isEmpty) {
+ throw new IllegalArgumentException(
+ s"${AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PATH.key} not configured for SSL connection")
+ }
+ if (trustStorePassword.isEmpty) {
+ throw new IllegalArgumentException(
+ s"${AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PASSWORD.key} not configured for SSL connection")
+ }
+
+ try {
+ val sslContext = getSSLContext(
+ trustStorePath.get,
+ trustStorePassword.get,
+ trustStoreType.getOrElse(KeyStore.getDefaultType))
+ LdapSSLSocketFactory.setSSLContextForCurrentThread(sslContext)
+ env.put(LdapSearchFactory.LDAP_SOCKET_FACTORY, classOf[LdapSSLSocketFactory].getName)
+ } catch {
+ case e @ (_: GeneralSecurityException | _: IOException) =>
+ val namingException = new NamingException("Failed to configure LDAP SSL context")
+ namingException.initCause(e)
+ throw namingException
+ }
+ }
+
+ private def getSSLContext(
+ trustStorePath: String,
+ trustStorePassword: String,
+ trustStoreType: String): SSLContext = {
+ LdapSSLUtils.createSSLContext(
+ trustStorePath,
+ trustStorePassword,
+ trustStoreType)
}
}
+
+object LdapSearchFactory {
+ private val LDAP_SOCKET_FACTORY = "java.naming.ldap.factory.socket"
+}
diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearchFactorySuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearchFactorySuite.scala
new file mode 100644
index 00000000000..fe059efaeb1
--- /dev/null
+++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/ldap/LdapSearchFactorySuite.scala
@@ -0,0 +1,183 @@
+/*
+ * 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.service.authentication.ldap
+
+import java.io.{ByteArrayInputStream, FileOutputStream, IOException}
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.security.KeyStore
+import java.security.cert.CertificateFactory
+import javax.naming.Context
+import javax.naming.directory.DirContext
+
+import org.scalatestplus.mockito.MockitoSugar.mock
+
+import org.apache.kyuubi.{KyuubiFunSuite, Utils}
+import org.apache.kyuubi.config.KyuubiConf
+import org.apache.kyuubi.config.KyuubiConf._
+
+class LdapSearchFactorySuite extends KyuubiFunSuite {
+
+ private val socketFactoryKey = "java.naming.ldap.factory.socket"
+ private val ldapUrl = "ldaps://127.0.0.1:1"
+ private val trustStorePassword = "password123"
+ private val trustStoreType = "PKCS12"
+
+ test("configure ldap ssl socket factory with PKCS12 truststore") {
+ val trustStorePath = createTrustStore(trustStoreType, trustStorePassword)
+ val conf = ldapSSLConf(trustStorePath)
+
+ assertSSLSocketFactoryConfigured(conf)
+ }
+
+ test("configure ldap ssl socket factory with PEM certificate truststore") {
+ val trustStorePath = createPemTrustStore()
+ val conf = ldapSSLConf(trustStorePath)
+
+ assertSSLSocketFactoryConfigured(conf)
+ }
+
+ test("do not configure ldap ssl socket factory when ldap ssl is disabled") {
+ val conf = KyuubiConf(loadSysDefault = false)
+ .set(AUTHENTICATION_LDAP_URL, ldapUrl)
+
+ val env = new LdapSearchFactory()
+ .createDirContextEnvironment(conf, "uid=user,ou=users", "password")
+
+ assert(env.get(Context.PROVIDER_URL) === ldapUrl)
+ assert(env.get(socketFactoryKey) === null)
+ }
+
+ test("require truststore path and password when ldap ssl is enabled") {
+ Seq(
+ KyuubiConf(loadSysDefault = false)
+ .set(AUTHENTICATION_LDAP_URL, ldapUrl)
+ .set(AUTHENTICATION_LDAP_SSL_ENABLE, true)
+ .set(AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PASSWORD, "password123") ->
+ AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PATH.key,
+ KyuubiConf(loadSysDefault = false)
+ .set(AUTHENTICATION_LDAP_URL, ldapUrl)
+ .set(AUTHENTICATION_LDAP_SSL_ENABLE, true)
+ .set(AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PATH, "/tmp/ldap.p12") ->
+ AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PASSWORD.key).foreach { case (conf, expectedKey) =>
+ val e = intercept[IllegalArgumentException] {
+ new LdapSearchFactory().createDirContextEnvironment(conf, "uid=user,ou=users", "password")
+ }
+ assert(e.getMessage.contains(expectedKey))
+ }
+ }
+
+ test("clear ldap ssl context when ldap search is closed") {
+ val trustStorePath = createTrustStore(trustStoreType, trustStorePassword)
+ val conf = ldapSSLConf(trustStorePath)
+
+ new LdapSearchFactory().createDirContextEnvironment(conf, "uid=user,ou=users", "password")
+ assert(LdapSSLSocketFactory.getDefault().isInstanceOf[LdapSSLSocketFactory])
+
+ new LdapSearch(conf, mock[DirContext], clearSslContextOnClose = true).close()
+
+ intercept[IllegalStateException] {
+ LdapSSLSocketFactory.getDefault()
+ }
+ }
+
+ test("report truststore loading error when truststore password is wrong") {
+ val trustStorePath = createTrustStore("PKCS12", "password123")
+
+ val e = intercept[IOException] {
+ LdapSSLUtils.createSSLContext(trustStorePath, "wrong-password", "PKCS12")
+ }
+
+ assert(e.getSuppressed.isEmpty)
+ }
+
+ private def createTrustStore(trustStoreType: String, password: String): String = {
+ val path = Utils.createTempDir().resolve(s"ldap-truststore.$trustStoreType")
+ val trustStore = KeyStore.getInstance(trustStoreType)
+ trustStore.load(null, password.toCharArray)
+ trustStore.setCertificateEntry("ldap", loadCertificate())
+ val out = new FileOutputStream(path.toFile)
+ try {
+ trustStore.store(out, password.toCharArray)
+ } finally {
+ out.close()
+ }
+ path.toAbsolutePath.toString
+ }
+
+ private def createPemTrustStore(): String = {
+ val path = Utils.createTempDir().resolve("ldap-truststore.pem")
+ Files.write(path, testCertificatePem.getBytes(StandardCharsets.UTF_8))
+ path.toAbsolutePath.toString
+ }
+
+ private def loadCertificate() = {
+ val certificateFactory = CertificateFactory.getInstance("X.509")
+ val in = new ByteArrayInputStream(testCertificatePem.getBytes(StandardCharsets.UTF_8))
+ try {
+ certificateFactory.generateCertificate(in)
+ } finally {
+ in.close()
+ }
+ }
+
+ private def ldapSSLConf(trustStorePath: String): KyuubiConf = {
+ KyuubiConf(loadSysDefault = false)
+ .set(AUTHENTICATION_LDAP_URL, ldapUrl)
+ .set(AUTHENTICATION_LDAP_SSL_ENABLE, true)
+ .set(AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PATH, trustStorePath)
+ .set(AUTHENTICATION_LDAP_SSL_TRUSTSTORE_PASSWORD, trustStorePassword)
+ .set(AUTHENTICATION_LDAP_SSL_TRUSTSTORE_TYPE, trustStoreType)
+ }
+
+ private def assertSSLSocketFactoryConfigured(conf: KyuubiConf): Unit = {
+ val env = new LdapSearchFactory()
+ .createDirContextEnvironment(conf, "uid=user,ou=users", "password")
+
+ assert(env.get(Context.PROVIDER_URL) === ldapUrl)
+ assert(env.get(socketFactoryKey) === classOf[LdapSSLSocketFactory].getName)
+ assert(LdapSSLSocketFactory.getDefault().isInstanceOf[LdapSSLSocketFactory])
+ }
+
+ private val testCertificatePem =
+ """-----BEGIN CERTIFICATE-----
+ |MIIDCTCCAfGgAwIBAgIUWpdT1drB26WnuUu6xQrRBRwRVWgwDQYJKoZIhvcNAQEL
+ |BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDczMTA4MDIyMloXDTM2MDcy
+ |ODA4MDIyMlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+ |AAOCAQ8AMIIBCgKCAQEA26bz4WtVhatDvNXzEkE9C5MpdKVop1lLtkFkgVcisdX4
+ |HcIM0Lv/1ryrofxvIgJjt8frzC0awX7NPrN55fLyQ+NtIt3xwdcE8TkdwNuStQpi
+ |R/TxpUtr+aZw40cDH35KeJNfEex6HKKbCWHTeItvwRhVt4vuATCavC/Tw9R5hF01
+ |c42txratWk83xg83Scy6sZwIhXZ9kGmoTvidaAT212q+onwjW087axd3npZ5iyjV
+ |pqkAZ4jzaqAlSuoEYHXsivqdAFIstx/BCqxPuIpfVh28fGBgovM0ROZFnCd/R0Pb
+ |YAQwmypNjiBHMJjiWjexqm7nDA2Nj6fIWpDQmN0kMQIDAQABo1MwUTAdBgNVHQ4E
+ |FgQUbtvmsQW8PbxcQ84f8SIMd9Ua7YswHwYDVR0jBBgwFoAUbtvmsQW8PbxcQ84f
+ |8SIMd9Ua7YswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAcs4I
+ |fP9tkF8L2gXkNE5pH/c8+zvH3j/QFj0pAz12YR02yjJKBXw/JKTxLAbOKZuyY6bV
+ |X4953ppMluvqm108IkOChkXPrD8nnF1WfgpZh61FXkboLWbZXS+BEb9j+5W+jeC3
+ |xeVZZG1GP9CiL6F4MdDnQaOuyRMY+iCM+zYbDe0r56my1UNIEymdXiEriy17lllw
+ |ho/IzPZKqt/JFGMQ8kkzWaYgF52cF6iuNdnYatd8HVcqYiyFG44XF5s3L9QHrZ5F
+ |1364kBO0IXST4cFNbyQ+DHekbQoUxT2SdHW2JN8TMvr+mjwVWRoH6lg7+4Mb/6JB
+ |36XlC2HY/TejOS5PyA==
+ |-----END CERTIFICATE-----
+ |""".stripMargin
+
+ override protected def afterEach(): Unit = {
+ LdapSSLSocketFactory.clearSslContextForCurrentThread()
+ super.afterEach()
+ }
+}