diff --git a/.gitignore b/.gitignore index 29815f67aaf..7c941e49b50 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ hs_err_pid* spark-warehouse/ metastore_db derby.log +ldap diff --git a/kyuubi-server/src/main/scala/org/apache/spark/KyuubiConf.scala b/kyuubi-server/src/main/scala/org/apache/spark/KyuubiConf.scala index d8bfcd0a2e5..0b70c706dec 100644 --- a/kyuubi-server/src/main/scala/org/apache/spark/KyuubiConf.scala +++ b/kyuubi-server/src/main/scala/org/apache/spark/KyuubiConf.scala @@ -300,7 +300,8 @@ object KyuubiConf { KyuubiConfigBuilder("spark.kyuubi.authentication") .doc("Client authentication types." + " NONE: no authentication check." + - " KERBEROS: Kerberos/GSSAPI authentication") + " KERBEROS: Kerberos/GSSAPI authentication." + + " LDAP: Lightweight Directory Access Protocol authentication.") .stringConf .createWithDefault("NONE") @@ -314,6 +315,24 @@ object KyuubiConf { .stringConf .createWithDefault("auth") + val AUTHENTICATION_LDAP_URL: ConfigEntry[String] = + KyuubiConfigBuilder("spark.kyuubi.authentication.ldap.url") + .doc("SPACE character separated LDAP connection URL(s).") + .stringConf + .createWithDefault("") + + val AUTHENTICATION_LDAP_BASEDN: ConfigEntry[String] = + KyuubiConfigBuilder("spark.kyuubi.authentication.ldap.baseDN") + .doc("LDAP base DN.") + .stringConf + .createWithDefault("") + + val AUTHENTICATION_LDAP_DOMAIN: ConfigEntry[String] = + KyuubiConfigBuilder("spark.kyuubi.authentication.ldap.Domain") + .doc("LDAP base DN.") + .stringConf + .createWithDefault("") + ///////////////////////////////////////////////////////////////////////////////////////////////// // Authorization // ///////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthMethods.scala b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthMethods.scala index 8f6ff3ff92a..b9ba5dd7a94 100644 --- a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthMethods.scala +++ b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthMethods.scala @@ -19,18 +19,15 @@ package yaooqinn.kyuubi.auth import javax.security.sasl.AuthenticationException -trait AuthMethods { - def authMethodStr: String = null -} +object AuthMethods extends Enumeration { -object AuthMethods { + type AuthMethods = Value - case object NONE extends AuthMethods { - override val authMethodStr = "NONE" - } + val NONE, LDAP = Value def getValidAuthMethod(authMethodStr: String): AuthMethods = authMethodStr match { - case NONE.authMethodStr => NONE + case "NONE" => NONE + case "LDAP" => LDAP case _ => throw new AuthenticationException("Not a valid authentication method") } } diff --git a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthType.scala b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthType.scala index 8a584b9c522..20bac080dba 100644 --- a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthType.scala +++ b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthType.scala @@ -19,30 +19,17 @@ package yaooqinn.kyuubi.auth import yaooqinn.kyuubi.service.ServiceException -abstract class AuthType { - def name: String = "" - override def toString: String = name +object AuthType extends Enumeration { -} - -object AuthType { - - case object NOSASL extends AuthType { - override val name: String = "NOSASL" - } + type AuthType = Value - case object NONE extends AuthType { - override val name: String = "NONE" - } - - case object KERBEROS extends AuthType { - override val name: String = "KERBEROS" - } + val NOSASL, NONE, LDAP, KERBEROS = Value def toAuthType(authTypeStr: String): AuthType = authTypeStr.toUpperCase match { - case NOSASL.name => NOSASL - case NONE.name => NONE - case KERBEROS.name => KERBEROS + case "NOSASL" => NOSASL + case "NONE" => NONE + case "LDAP" => LDAP + case "KERBEROS" => KERBEROS case other => throw new ServiceException("Unsupported authentication method: " + other) } } diff --git a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthenticationProviderFactory.scala b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthenticationProviderFactory.scala index 917c2e66c28..35697eb226c 100644 --- a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthenticationProviderFactory.scala +++ b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/AuthenticationProviderFactory.scala @@ -18,14 +18,20 @@ package yaooqinn.kyuubi.auth import javax.security.sasl.AuthenticationException +import org.apache.spark.SparkConf + +import yaooqinn.kyuubi.auth.AuthMethods.AuthMethods + /** * This class helps select a [[PasswdAuthenticationProvider]] for a given [[AuthMethods]] */ object AuthenticationProviderFactory { @throws[AuthenticationException] - def getAuthenticationProvider(method: AuthMethods): PasswdAuthenticationProvider = method match { + def getAuthenticationProvider( + method: AuthMethods, + conf: SparkConf): PasswdAuthenticationProvider = method match { case AuthMethods.NONE => new AnonymousAuthenticationProviderImpl + case AuthMethods.LDAP => new LdapAuthenticationProviderImpl(conf) case _ => throw new AuthenticationException("Not a valid authentication method") - } } diff --git a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/KyuubiAuthFactory.scala b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/KyuubiAuthFactory.scala index 4a7d79d9961..4e942078f47 100644 --- a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/KyuubiAuthFactory.scala +++ b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/KyuubiAuthFactory.scala @@ -65,7 +65,7 @@ class KyuubiAuthFactory(conf: SparkConf) extends Logging { throw new TTransportException("Failed to start token manager", e) } Some(server) - case AuthType.NONE | AuthType.NOSASL => None + case AuthType.NONE | AuthType.NOSASL | AuthType.LDAP => None case other => throw new ServiceException("Unsupported authentication method: " + other) } @@ -86,7 +86,7 @@ class KyuubiAuthFactory(conf: SparkConf) extends Logging { } case _ => authMethod match { case AuthType.NOSASL => new TTransportFactory - case _ => PlainSaslHelper.getTransportFactory(authMethod.name) + case _ => PlainSaslHelper.getTransportFactory(authMethod.toString, conf) } } diff --git a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/LdapAuthenticationProviderImpl.scala b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/LdapAuthenticationProviderImpl.scala new file mode 100644 index 00000000000..f96634142d0 --- /dev/null +++ b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/LdapAuthenticationProviderImpl.scala @@ -0,0 +1,87 @@ +/* + * 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 yaooqinn.kyuubi.auth + +import java.util.Hashtable +import javax.naming.{Context, NamingException} +import javax.naming.directory.InitialDirContext +import javax.security.sasl.AuthenticationException + +import org.apache.commons.lang3.StringUtils +import org.apache.spark.{KyuubiConf, SparkConf} + +import yaooqinn.kyuubi.service.ServiceUtils + +class LdapAuthenticationProviderImpl(conf: SparkConf) extends PasswdAuthenticationProvider { + + import KyuubiConf._ + + /** + * The authenticate method is called by the Kyuubi Server authentication layer + * to authenticate users for their requests. + * If a user is to be granted, return nothing/throw nothing. + * When a user is to be disallowed, throw an appropriate [[AuthenticationException]]. + * + * @param user The username received over the connection request + * @param password The password received over the connection request + * + * @throws AuthenticationException When a user is found to be invalid by the implementation + */ + override def authenticate(user: String, password: String): Unit = { + if (StringUtils.isBlank(user)) { + throw new AuthenticationException(s"Error validating LDAP user, user is null" + + s" or contains blank space") + } + + if (StringUtils.isBlank(password)) { + throw new AuthenticationException(s"Error validating LDAP user, password is null" + + s" or contains blank space") + } + + val env = new Hashtable[String, Any]() + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory") + env.put(Context.SECURITY_AUTHENTICATION, "simple") + + conf.getOption(AUTHENTICATION_LDAP_URL).foreach(env.put(Context.PROVIDER_URL, _)) + + val domain = conf.get(AUTHENTICATION_LDAP_DOMAIN, "") + val u = if (!hasDomain(user) && StringUtils.isNotBlank(domain)) { + user + "@" + domain + } else { + user + } + + val bindDn = conf.getOption(AUTHENTICATION_LDAP_BASEDN) match { + case Some(dn) => "uid=" + u + "," + dn + case _ => u + } + + env.put(Context.SECURITY_PRINCIPAL, bindDn) + env.put(Context.SECURITY_CREDENTIALS, password) + + try { + val ctx = new InitialDirContext(env) + ctx.close() + } catch { + case e: NamingException => + throw new AuthenticationException(s"Error validating LDAP user: $bindDn", e) + } + } + + private def hasDomain(userName: String): Boolean = ServiceUtils.indexOfDomainMatch(userName) > 0 +} diff --git a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/PlainSaslHelper.scala b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/PlainSaslHelper.scala index adb9de7ee81..f37db78ef6b 100644 --- a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/PlainSaslHelper.scala +++ b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/PlainSaslHelper.scala @@ -25,9 +25,11 @@ import javax.security.sasl.{AuthenticationException, AuthorizeCallback} import scala.collection.JavaConverters._ import org.apache.hive.service.cli.thrift.TCLIService.Iface +import org.apache.spark.SparkConf import org.apache.thrift.{TProcessor, TProcessorFactory} import org.apache.thrift.transport.{TSaslServerTransport, TTransport, TTransportFactory} +import yaooqinn.kyuubi.auth.AuthMethods.AuthMethods import yaooqinn.kyuubi.auth.PlainSaslServer.SaslPlainProvider object PlainSaslHelper { @@ -41,10 +43,10 @@ object PlainSaslHelper { } @throws[LoginException] - def getTransportFactory(authTypeStr: String): TTransportFactory = { + def getTransportFactory(authTypeStr: String, conf: SparkConf): TTransportFactory = { val saslFactory = new TSaslServerTransport.Factory() try { - val handler = new PlainServerCallbackHandler(authTypeStr) + val handler = new PlainServerCallbackHandler(authTypeStr, conf) val props = Map.empty[String, String] saslFactory.addServerDefinition("PLAIN", authTypeStr, null, props.asJava, handler) } catch { @@ -54,10 +56,11 @@ object PlainSaslHelper { saslFactory } - private class PlainServerCallbackHandler private(authMethod: AuthMethods) + private class PlainServerCallbackHandler private(authMethod: AuthMethods, conf: SparkConf) extends CallbackHandler { @throws[AuthenticationException] - def this(authMethodStr: String) = this(AuthMethods.getValidAuthMethod(authMethodStr)) + def this(authMethodStr: String, conf: SparkConf) = + this(AuthMethods.getValidAuthMethod(authMethodStr), conf) @throws[IOException] @throws[UnsupportedCallbackException] @@ -75,7 +78,7 @@ object PlainSaslHelper { case _ => throw new UnsupportedCallbackException(callback) } } - val provider = AuthenticationProviderFactory.getAuthenticationProvider(authMethod) + val provider = AuthenticationProviderFactory.getAuthenticationProvider(authMethod, conf) provider.authenticate(username, password) if (ac != null) ac.setAuthorized(true) } diff --git a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/PlainSaslServer.scala b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/PlainSaslServer.scala index ecef8814ef4..917c20b8e17 100644 --- a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/PlainSaslServer.scala +++ b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/auth/PlainSaslServer.scala @@ -22,6 +22,8 @@ import java.util.{ArrayDeque => JDeque, Map => JMap} import javax.security.auth.callback._ import javax.security.sasl.{AuthorizeCallback, SaslException, SaslServer, SaslServerFactory} +import yaooqinn.kyuubi.auth.AuthMethods.AuthMethods + object PlainSaslServer { val PLAIN_METHOD = "PLAIN" @@ -70,7 +72,7 @@ class PlainSaslServer(handler: CallbackHandler, authMethod: AuthMethods) extends case 0 => tokenList.addLast(messageToken.toString) messageToken.setLength(0) - case b: Byte => messageToken.append(b) + case b: Byte => messageToken.append(b.toChar) } tokenList.addLast(messageToken.toString) // validate response diff --git a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/server/FrontendService.scala b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/server/FrontendService.scala index bed6ff6019f..2341620a6c7 100644 --- a/kyuubi-server/src/main/scala/yaooqinn/kyuubi/server/FrontendService.scala +++ b/kyuubi-server/src/main/scala/yaooqinn/kyuubi/server/FrontendService.scala @@ -151,7 +151,7 @@ private[kyuubi] class FrontendService private(name: String, beService: BackendSe def getServerIPAddress: InetAddress = serverIPAddress private def isKerberosAuthMode: Boolean = { - conf.get(KyuubiConf.AUTHENTICATION_METHOD).equalsIgnoreCase(AuthType.KERBEROS.name) + conf.get(KyuubiConf.AUTHENTICATION_METHOD).equalsIgnoreCase(AuthType.KERBEROS.toString) } private def getUserName(req: TOpenSessionReq): String = { diff --git a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthMethodsSuite.scala b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthMethodsSuite.scala index c63511f2223..9a2ce20eee0 100644 --- a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthMethodsSuite.scala +++ b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthMethodsSuite.scala @@ -21,14 +21,17 @@ import javax.security.sasl.AuthenticationException import org.apache.spark.SparkFunSuite +import yaooqinn.kyuubi.auth.AuthMethods._ + class AuthMethodsSuite extends SparkFunSuite { - test("testGetValidAuthMethod") { - assert(new AuthMethods{}.authMethodStr === null) - assert(AuthMethods.getValidAuthMethod("NONE") === AuthMethods.NONE) - assert(AuthMethods.getValidAuthMethod(AuthMethods.NONE.authMethodStr) === AuthMethods.NONE) + test("get valid auth method") { + assert(getValidAuthMethod("NONE") === NONE) + assert(getValidAuthMethod(NONE.toString) === NONE) + assert(getValidAuthMethod("LDAP") === LDAP) + assert(getValidAuthMethod(LDAP.toString) === LDAP) - assert(AuthMethods.NONE.authMethodStr === "NONE") - intercept[AuthenticationException](AuthMethods.getValidAuthMethod("ELSE")) + assert(NONE.toString === "NONE") + intercept[AuthenticationException](getValidAuthMethod("ELSE")) } } diff --git a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthTypeSuite.scala b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthTypeSuite.scala index 9d5797ca685..a3f16ae1900 100644 --- a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthTypeSuite.scala +++ b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthTypeSuite.scala @@ -19,27 +19,24 @@ package yaooqinn.kyuubi.auth import org.apache.spark.SparkFunSuite +import yaooqinn.kyuubi.auth.AuthType._ import yaooqinn.kyuubi.service.ServiceException class AuthTypeSuite extends SparkFunSuite { - test("test name") { - assert(new AuthType {}.name === "") - assert(AuthType.NONE.name === "NONE") - assert(AuthType.KERBEROS.name === "KERBEROS") - assert(AuthType.NOSASL.name === "NOSASL") - } - test("to auth type") { - assert(AuthType.toAuthType("NONE") === AuthType.NONE) - assert(AuthType.toAuthType(AuthType.NONE.name) === AuthType.NONE) + assert(toAuthType("NOSASL") === NOSASL) + assert(toAuthType(NOSASL.toString) === NOSASL) + + assert(toAuthType("NONE") === NONE) + assert(toAuthType(NONE.toString) === NONE) - assert(AuthType.toAuthType("KERBEROS") === AuthType.KERBEROS) - assert(AuthType.toAuthType(AuthType.KERBEROS.name) === AuthType.KERBEROS) + assert(toAuthType("LDAP") === LDAP) + assert(toAuthType(LDAP.toString) === LDAP) - assert(AuthType.toAuthType("NOSASL") === AuthType.NOSASL) - assert(AuthType.toAuthType(AuthType.NOSASL.name) === AuthType.NOSASL) + assert(toAuthType("KERBEROS") === KERBEROS) + assert(toAuthType(KERBEROS.toString) === KERBEROS) - intercept[ServiceException](AuthType.toAuthType("ELSE")) + intercept[ServiceException](toAuthType("ELSE")) } } diff --git a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthenticationProviderFactorySuite.scala b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthenticationProviderFactorySuite.scala index 681902a4a82..fae185b3888 100644 --- a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthenticationProviderFactorySuite.scala +++ b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/AuthenticationProviderFactorySuite.scala @@ -19,17 +19,22 @@ package yaooqinn.kyuubi.auth import javax.security.sasl.AuthenticationException -import org.apache.spark.SparkFunSuite +import org.apache.spark.{SparkConf, SparkFunSuite} class AuthenticationProviderFactorySuite extends SparkFunSuite { test("testGetAuthenticationProvider") { - AuthenticationProviderFactory - .getAuthenticationProvider(AuthMethods.NONE).authenticate("test", "test") - intercept[AuthenticationException](AuthenticationProviderFactory - .getAuthenticationProvider(new AuthMethods { - override def authMethodStr: String = "TEST" - })) + val conf = new SparkConf() + val anonymous = AuthenticationProviderFactory.getAuthenticationProvider(AuthMethods.NONE, conf) + anonymous.authenticate("test", "test") + + val ldap = AuthenticationProviderFactory.getAuthenticationProvider(AuthMethods.LDAP, conf) + val exception = intercept[AuthenticationException](ldap.authenticate("test", "test")) + assert(exception.getMessage.contains("Error validating LDAP user:")) + + val e2 = intercept[AuthenticationException]( + AuthenticationProviderFactory.getAuthenticationProvider(null, conf)) + assert(e2.getMessage === "Not a valid authentication method") } } diff --git a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/KyuubiAuthFactorySuite.scala b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/KyuubiAuthFactorySuite.scala index f9c4b2c8198..b128c171dc4 100644 --- a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/KyuubiAuthFactorySuite.scala +++ b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/KyuubiAuthFactorySuite.scala @@ -25,7 +25,6 @@ import org.apache.hadoop.security.authorize.AuthorizationException import org.apache.spark.{KyuubiConf, KyuubiSparkUtil, SparkConf, SparkFunSuite} import yaooqinn.kyuubi.KyuubiSQLException -import yaooqinn.kyuubi.server.KyuubiServer import yaooqinn.kyuubi.service.ServiceException import yaooqinn.kyuubi.utils.ReflectUtils @@ -81,6 +80,13 @@ class KyuubiAuthFactorySuite extends SparkFunSuite { assert(e.getMessage === "Unsupported authentication method: OTHER") } + test("AuthType LDAP") { + val conf = new SparkConf(true).set(KyuubiConf.AUTHENTICATION_METHOD.key, "LDAP") + KyuubiSparkUtil.setupCommonConfig(conf) + val authFactory = new KyuubiAuthFactory(conf) + assert(authFactory.getIpAddress.isEmpty) + } + test("AuthType KERBEROS without keytab/principal") { val conf = new SparkConf(true).set(KyuubiConf.AUTHENTICATION_METHOD.key, "KERBEROS") KyuubiSparkUtil.setupCommonConfig(conf) diff --git a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/LdapAuthenticationProviderImplSuite.scala b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/LdapAuthenticationProviderImplSuite.scala new file mode 100644 index 00000000000..b86fd64960a --- /dev/null +++ b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/LdapAuthenticationProviderImplSuite.scala @@ -0,0 +1,187 @@ +/* + * 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 yaooqinn.kyuubi.auth + +import java.io.File +import java.util.{Collections, Hashtable} +import javax.naming.{CommunicationException, Context} +import javax.naming.directory.{BasicAttribute, BasicAttributes, InitialDirContext} +import javax.security.sasl.AuthenticationException + +import scala.collection.mutable.ArrayBuffer + +import org.apache.directory.api.ldap.model.name.Dn +import org.apache.directory.api.ldap.schemaextractor.impl.DefaultSchemaLdifExtractor +import org.apache.directory.api.ldap.schemaloader.LdifSchemaLoader +import org.apache.directory.api.ldap.schemamanager.impl.DefaultSchemaManager +import org.apache.directory.server.constants.ServerDNConstants +import org.apache.directory.server.core.DefaultDirectoryService +import org.apache.directory.server.core.api.{CacheService, InstanceLayout} +import org.apache.directory.server.core.api.partition.Partition +import org.apache.directory.server.core.api.schema.SchemaPartition +import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition +import org.apache.directory.server.core.partition.ldif.LdifPartition +import org.apache.directory.server.ldap.LdapServer +import org.apache.directory.server.protocol.shared.transport.TcpTransport +import org.apache.mina.util.AvailablePortFinder +import org.apache.spark.{KyuubiConf, KyuubiSparkUtil, SparkConf, SparkFunSuite} + +class LdapAuthenticationProviderImplSuite extends SparkFunSuite { + + import KyuubiConf._ + + private val servicePort = AvailablePortFinder.getNextAvailable(9000) + val service = new DefaultDirectoryService + private val workingDIr = KyuubiSparkUtil.createTempDir() + val ldapServer: LdapServer = new LdapServer() + + val partitions = ArrayBuffer[Partition]() + + val conf = new SparkConf() + + override def beforeAll(): Unit = { + service.setInstanceLayout(new InstanceLayout(workingDIr)) + val cachedService = new CacheService() + cachedService.initialize(service.getInstanceLayout) + service.setCacheService(cachedService) + + val schemaPartitionDir = new File(service.getInstanceLayout.getPartitionsDirectory, "schema") + if (!schemaPartitionDir.exists()) { + val extractor = new DefaultSchemaLdifExtractor( + service.getInstanceLayout.getPartitionsDirectory) + extractor.extractOrCopy() + } + + val loader = new LdifSchemaLoader(schemaPartitionDir) + val schemaManager = new DefaultSchemaManager(loader) + schemaManager.loadAllEnabled() + service.setSchemaManager(schemaManager) + + val schemaLDifPartition = new LdifPartition(schemaManager) + schemaLDifPartition.setPartitionPath(schemaPartitionDir.toURI) + val schemaPartition = new SchemaPartition(schemaManager) + schemaPartition.setWrappedPartition(schemaLDifPartition) + service.setSchemaPartition(schemaPartition) + + val jdbmPartition = new JdbmPartition(service.getSchemaManager) + jdbmPartition.setId("kyuubi") + jdbmPartition.setPartitionPath( + new File(service.getInstanceLayout.getPartitionsDirectory, jdbmPartition.getId).toURI) + jdbmPartition.setSuffixDn(new Dn(ServerDNConstants.SYSTEM_DN)) + jdbmPartition.setSchemaManager(service.getSchemaManager) + service.setSystemPartition(jdbmPartition) + + service.getChangeLog.setEnabled(false) + + service.setDenormalizeOpAttrsEnabled(true) + + service.startup() + ldapServer.setDirectoryService(service) + ldapServer.setTransports(new TcpTransport(servicePort)) + ldapServer.start() + + conf.set(AUTHENTICATION_LDAP_URL, "ldap://localhost:" + ldapServer.getPort) + + addPartition("kentyao", "uid=kentyao,ou=users") + addUser() + + super.beforeAll() + } + + private def addUser(): Unit = { + val env = new Hashtable[String, Any]() + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory") + env.put(Context.SECURITY_AUTHENTICATION, "simple") + env.put(Context.PROVIDER_URL, conf.get(AUTHENTICATION_LDAP_URL)) + env.put(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system") + env.put(Context.SECURITY_CREDENTIALS, "secret") + + val container = new BasicAttributes() + val objClasses = new BasicAttribute("objectClass") + objClasses.add("inetOrgPerson") + val cn = new BasicAttribute("cn", "kentyao") + val o = new BasicAttribute("o", "kyuubi") + val sn = new BasicAttribute("sn", "yao") + val uid = new BasicAttribute("uid", "kentyao") + val password = new BasicAttribute("userpassword", "kentyao") + container.put(objClasses) + container.put(cn) + container.put(uid) + container.put(o) + container.put(sn) + container.put(password) + val context = new InitialDirContext(env) + context.createSubcontext("uid=kentyao,ou=users", container) + } + + private def addPartition(id: String, dn: String): Partition = { + val partition = new JdbmPartition(service.getSchemaManager) + partition.setId(id) + partition.setPartitionPath( + new File(service.getInstanceLayout.getPartitionsDirectory, partition.getId).toURI) + partition.setSuffixDn(new Dn(dn)) + service.addPartition(partition) + partitions += partition + partition + } + + override def afterAll(): Unit = { + if (ldapServer.isStarted) { + ldapServer.stop() + } + super.afterAll() + } + + test("ldap server is started") { + assert(ldapServer.isStarted) + assert(ldapServer.getPort > 0) + } + + test("authenticate tests") { + val providerImpl = new LdapAuthenticationProviderImpl(conf) + val e1 = intercept[AuthenticationException](providerImpl.authenticate("", "")) + assert(e1.getMessage.contains("user is null")) + val e2 = intercept[AuthenticationException](providerImpl.authenticate("kyuubi", "")) + assert(e2.getMessage.contains("password is null")) + + val user = "uid=kentyao,ou=users" + providerImpl.authenticate(user, "kentyao") + val e3 = intercept[AuthenticationException]( + providerImpl.authenticate(user, "kent")) + assert(e3.getMessage.contains(user)) + assert(e3.getCause.isInstanceOf[javax.naming.AuthenticationException]) + + val dn = "ou=users" + conf.set(AUTHENTICATION_LDAP_BASEDN, dn) + val providerImpl2 = new LdapAuthenticationProviderImpl(conf) + providerImpl2.authenticate("kentyao", "kentyao") + + val e4 = intercept[AuthenticationException]( + providerImpl.authenticate("kentyao", "kent")) + assert(e4.getMessage.contains(user)) + + conf.remove(AUTHENTICATION_LDAP_URL) + val providerImpl3 = new LdapAuthenticationProviderImpl(conf) + val e5 = intercept[AuthenticationException]( + providerImpl3.authenticate("kentyao", "kentyao")) + + assert(e5.getMessage.contains(user)) + assert(e5.getCause.isInstanceOf[CommunicationException]) + } + +} diff --git a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/PlainSaslHelperSuite.scala b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/PlainSaslHelperSuite.scala index 087c66a8eee..265628e57e5 100644 --- a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/PlainSaslHelperSuite.scala +++ b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/PlainSaslHelperSuite.scala @@ -38,10 +38,10 @@ class PlainSaslHelperSuite extends SparkFunSuite { val tSocket = new TSocket("0.0.0.0", 0) val tProcessor = tProcessorFactory.getProcessor(tSocket) assert(tProcessor.isInstanceOf[TSetIpAddressProcessor[_]]) - intercept[LoginException](PlainSaslHelper.getTransportFactory("KERBEROS")) - intercept[LoginException](PlainSaslHelper.getTransportFactory("NOSASL")) - intercept[LoginException](PlainSaslHelper.getTransportFactory("ELSE")) - val tTransportFactory = PlainSaslHelper.getTransportFactory("NONE") + intercept[LoginException](PlainSaslHelper.getTransportFactory("KERBEROS", conf)) + intercept[LoginException](PlainSaslHelper.getTransportFactory("NOSASL", conf)) + intercept[LoginException](PlainSaslHelper.getTransportFactory("ELSE", conf)) + val tTransportFactory = PlainSaslHelper.getTransportFactory("NONE", conf) assert(tTransportFactory.isInstanceOf[TSaslServerTransport.Factory]) Security.getProviders.exists(_.isInstanceOf[SaslPlainProvider]) } diff --git a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/PlainSaslServerSuite.scala b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/PlainSaslServerSuite.scala index e36c680620b..409a3d4e6de 100644 --- a/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/PlainSaslServerSuite.scala +++ b/kyuubi-server/src/test/scala/yaooqinn/kyuubi/auth/PlainSaslServerSuite.scala @@ -64,10 +64,10 @@ class PlainSaslServerSuite extends SparkFunSuite { val e3 = intercept[SaslException](server.evaluateResponse(res3.map(_.toByte))) assert(e3.getMessage === "Error validating the login") assert(e3.getCause.getMessage === "No password name provided") - val res4 = Array(1, 0, 1) + val res4 = Array(97, 0, 99) server.evaluateResponse(res4.map(_.toByte)) assert(server.isComplete) - assert(server.getAuthorizationID === "1") + assert(server.getAuthorizationID === "a") intercept[UnsupportedOperationException](server.wrap(res4.map(_.toByte), 0, 3)) intercept[UnsupportedOperationException](server.unwrap(res4.map(_.toByte), 0, 3)) assert(server.getNegotiatedProperty("name") === null) diff --git a/pom.xml b/pom.xml index 2b14acee831..4b985fb991a 100644 --- a/pom.xml +++ b/pom.xml @@ -291,14 +291,14 @@ test - org.apache.directory.api - api-all + org.apache.directory.server + * - org.apache.directory.jdbm - apacheds-jdbm1 + org.apache.directory.api + api-all - + org.apache.directory.server