diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md index b3a9ff337db..58890b14fa0 100644 --- a/docs/configuration/settings.md +++ b/docs/configuration/settings.md @@ -246,6 +246,8 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co | kyuubi.frontend.mysql.worker.keepalive.time | PT1M | Time(ms) that an idle async thread of the command execution thread pool will wait for a new task to arrive before terminating in MySQL frontend service | duration | 1.4.0 | | kyuubi.frontend.protocols | THRIFT_BINARY,REST | A comma-separated list for all frontend protocols | seq | 1.4.0 | | kyuubi.frontend.proxy.http.client.ip.header | X-Real-IP | The HTTP header to record the real client IP address. If your server is behind a load balancer or other proxy, the server will see this load balancer or proxy IP address as the client IP address, to get around this common issue, most load balancers or proxies offer the ability to record the real remote IP address in an HTTP header that will be added to the request for other devices to use. Note that, because the header value can be specified to any IP address, so it will not be used for authentication. | string | 1.6.0 | +| kyuubi.frontend.rest.authentication | NONE | A comma-separated list of rest protocol client authentication types. It fallback to `kyuubi.authentication` if not configure. | seq | 1.9.0 | +| kyuubi.frontend.rest.authentication.custom.class | <undefined> | User-defined authentication implementation of org.apache.kyuubi.service.authentication.PasswdAuthenticationProvider for rest protocol. | string | 1.9.0 | | kyuubi.frontend.rest.bind.host | <undefined> | Hostname or IP of the machine on which to run the REST frontend service. | string | 1.4.0 | | kyuubi.frontend.rest.bind.port | 10099 | Port of the machine on which to run the REST frontend service. | int | 1.4.0 | | kyuubi.frontend.rest.max.worker.threads | 999 | Maximum number of threads in the frontend worker thread pool for the rest frontend service | int | 1.6.2 | 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 9f09e9bb636..f2e4a4db988 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 @@ -797,6 +797,14 @@ object KyuubiConf { .checkValues(AuthTypes) .createWithDefault(Seq(AuthTypes.NONE.toString)) + val FRONTEND_REST_AUTHENTICATION_METHOD: ConfigEntry[Seq[String]] = + buildConf("kyuubi.frontend.rest.authentication") + .doc("A comma-separated list of rest protocol client authentication types." + + " It fallback to `kyuubi.authentication` if not configure.") + .version("1.9.0") + .serverOnly + .fallbackConf(AUTHENTICATION_METHOD) + val AUTHENTICATION_CUSTOM_CLASS: OptionalConfigEntry[String] = buildConf("kyuubi.authentication.custom.class") .doc("User-defined authentication implementation of " + @@ -806,6 +814,14 @@ object KyuubiConf { .stringConf .createOptional + val FRONTEND_REST_AUTHENTICATION_CUSTOM_CLASS: ConfigEntry[Option[String]] = + buildConf("kyuubi.frontend.rest.authentication.custom.class") + .doc("User-defined authentication implementation of " + + "org.apache.kyuubi.service.authentication.PasswdAuthenticationProvider for rest protocol.") + .version("1.9.0") + .serverOnly + .fallbackConf(AUTHENTICATION_CUSTOM_CLASS) + val AUTHENTICATION_LDAP_URL: OptionalConfigEntry[String] = buildConf("kyuubi.authentication.ldap.url") .doc("SPACE character separated LDAP connection URL(s).") diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala index 9aefe63c8b6..77b2f4ceec0 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/TFrontendService.scala @@ -28,6 +28,7 @@ import org.apache.hadoop.conf.Configuration import org.apache.kyuubi.{KyuubiSQLException, Logging, Utils} import org.apache.kyuubi.Utils.stringifyException import org.apache.kyuubi.config.KyuubiConf.{FRONTEND_ADVERTISED_HOST, FRONTEND_CONNECTION_URL_USE_HOSTNAME, PROXY_USER, SESSION_CLOSE_ON_DISCONNECT} +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.THRIFT_BINARY import org.apache.kyuubi.config.KyuubiReservedKeys._ import org.apache.kyuubi.operation.{FetchOrientation, OperationHandle} import org.apache.kyuubi.service.authentication.{AuthUtils, KyuubiAuthenticationFactory} @@ -57,7 +58,7 @@ abstract class TFrontendService(name: String) protected lazy val serverSocket = new ServerSocket(portNum, -1, serverAddr) protected lazy val actualPort: Int = serverSocket.getLocalPort protected lazy val authFactory: KyuubiAuthenticationFactory = - new KyuubiAuthenticationFactory(conf, isServer()) + new KyuubiAuthenticationFactory(conf, THRIFT_BINARY, isServer()) protected def hadoopConf: Configuration = _hadoopConf diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/AuthenticationProviderFactory.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/AuthenticationProviderFactory.scala index ffdd9b8bb90..7262e2dbb2f 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/AuthenticationProviderFactory.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/AuthenticationProviderFactory.scala @@ -20,6 +20,7 @@ package org.apache.kyuubi.service.authentication import javax.security.sasl.AuthenticationException import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.{FrontendProtocol, REST} import org.apache.kyuubi.service.authentication.AuthMethods.AuthMethod import org.apache.kyuubi.util.ClassUtils @@ -31,9 +32,10 @@ object AuthenticationProviderFactory { def getAuthenticationProvider( method: AuthMethod, conf: KyuubiConf, + protocol: FrontendProtocol, isServer: Boolean = true): PasswdAuthenticationProvider = { if (isServer) { - getAuthenticationProviderForServer(method, conf) + getAuthenticationProviderForServer(method, conf, protocol) } else { getAuthenticationProviderForEngine(conf) } @@ -41,12 +43,16 @@ object AuthenticationProviderFactory { private def getAuthenticationProviderForServer( method: AuthMethod, - conf: KyuubiConf): PasswdAuthenticationProvider = method match { + conf: KyuubiConf, + protocol: FrontendProtocol): PasswdAuthenticationProvider = method match { case AuthMethods.NONE => new AnonymousAuthenticationProviderImpl case AuthMethods.LDAP => new LdapAuthenticationProviderImpl(conf) case AuthMethods.JDBC => new JdbcAuthenticationProviderImpl(conf) case AuthMethods.CUSTOM => - val className = conf.get(KyuubiConf.AUTHENTICATION_CUSTOM_CLASS) + val className = protocol match { + case REST => conf.get(KyuubiConf.FRONTEND_REST_AUTHENTICATION_CUSTOM_CLASS) + case _ => conf.get(KyuubiConf.AUTHENTICATION_CUSTOM_CLASS) + } if (className.isEmpty) { throw new AuthenticationException( "authentication.custom.class must be set when auth method was CUSTOM.") diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala index 978527b8818..3a846f8bfd1 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactory.scala @@ -24,12 +24,16 @@ import javax.security.sasl.Sasl import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.FrontendProtocol import org.apache.kyuubi.service.authentication.AuthTypes._ import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.Iface import org.apache.kyuubi.shaded.thrift.TProcessorFactory import org.apache.kyuubi.shaded.thrift.transport.{TSaslServerTransport, TTransportException, TTransportFactory} -class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) extends Logging { +class KyuubiAuthenticationFactory( + conf: KyuubiConf, + protocol: FrontendProtocol, + isServer: Boolean = true) extends Logging { val authTypes: Seq[AuthType] = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName) val saslDisabled: Boolean = AuthUtils.saslDisabled(authTypes) @@ -85,6 +89,7 @@ class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) ex transportFactory = PlainSASLHelper.getTransportFactory( plainAuthType.toString, conf, + protocol, Option(transportFactory), isServer).asInstanceOf[TSaslServerTransport.Factory] diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/PlainSASLHelper.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/PlainSASLHelper.scala index 2d880a344e6..7d3efeab7cc 100644 --- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/PlainSASLHelper.scala +++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/PlainSASLHelper.scala @@ -23,6 +23,7 @@ import javax.security.auth.callback.{Callback, CallbackHandler, NameCallback, Pa import javax.security.sasl.AuthorizeCallback import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.FrontendProtocol import org.apache.kyuubi.service.authentication.AuthMethods.AuthMethod import org.apache.kyuubi.service.authentication.PlainSASLServer.SaslPlainProvider import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TCLIService.Iface @@ -42,11 +43,16 @@ object PlainSASLHelper { private class PlainServerCallbackHandler private ( authMethod: AuthMethod, conf: KyuubiConf, + protocol: FrontendProtocol, isServer: Boolean) extends CallbackHandler { - def this(authMethodStr: String, conf: KyuubiConf, isServer: Boolean) = - this(AuthMethods.withName(authMethodStr), conf, isServer) + def this( + authMethodStr: String, + conf: KyuubiConf, + protocol: FrontendProtocol, + isServer: Boolean) = + this(AuthMethods.withName(authMethodStr), conf, protocol, isServer) @throws[UnsupportedCallbackException] override def handle(callbacks: Array[Callback]): Unit = { @@ -64,7 +70,11 @@ object PlainSASLHelper { } } val provider = - AuthenticationProviderFactory.getAuthenticationProvider(authMethod, conf, isServer) + AuthenticationProviderFactory.getAuthenticationProvider( + authMethod, + conf, + protocol, + isServer) provider.authenticate(username, password) if (ac != null) ac.setAuthorized(true) } @@ -77,11 +87,12 @@ object PlainSASLHelper { def getTransportFactory( authTypeStr: String, conf: KyuubiConf, + protocol: FrontendProtocol, transportFactory: Option[TSaslServerTransport.Factory] = None, isServer: Boolean = true): TTransportFactory = { val saslFactory = transportFactory.getOrElse(new TSaslServerTransport.Factory()) try { - val handler = new PlainServerCallbackHandler(authTypeStr, conf, isServer) + val handler = new PlainServerCallbackHandler(authTypeStr, conf, protocol, isServer) val props = new java.util.HashMap[String, String] saslFactory.addServerDefinition("PLAIN", authTypeStr, null, props, handler) } catch { diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/AuthenticationProviderFactorySuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/AuthenticationProviderFactorySuite.scala index 0bd29ac56a9..b8de4eaa880 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/AuthenticationProviderFactorySuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/AuthenticationProviderFactorySuite.scala @@ -21,6 +21,7 @@ import javax.security.sasl.AuthenticationException import org.apache.kyuubi.{KyuubiFunSuite, Utils} import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.THRIFT_BINARY class AuthenticationProviderFactorySuite extends KyuubiFunSuite { @@ -28,13 +29,13 @@ class AuthenticationProviderFactorySuite extends KyuubiFunSuite { test("get auth provider") { val conf = KyuubiConf() - val p1 = getAuthenticationProvider(AuthMethods.withName("NONE"), conf) + val p1 = getAuthenticationProvider(AuthMethods.withName("NONE"), conf, THRIFT_BINARY) p1.authenticate(Utils.currentUser, "") - val p2 = getAuthenticationProvider(AuthMethods.withName("LDAP"), conf) + val p2 = getAuthenticationProvider(AuthMethods.withName("LDAP"), conf, THRIFT_BINARY) val e1 = intercept[AuthenticationException](p2.authenticate("test", "test")) assert(e1.getMessage.contains("Error validating LDAP user:")) val e2 = intercept[AuthenticationException]( - AuthenticationProviderFactory.getAuthenticationProvider(null, conf)) + AuthenticationProviderFactory.getAuthenticationProvider(null, conf, THRIFT_BINARY)) assert(e2.getMessage === "Not a valid authentication method") } diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/CustomAuthenticationProviderImplSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/CustomAuthenticationProviderImplSuite.scala index 6135535e1d7..68457e7667f 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/CustomAuthenticationProviderImplSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/CustomAuthenticationProviderImplSuite.scala @@ -21,6 +21,7 @@ import javax.security.sasl.AuthenticationException import org.apache.kyuubi.KyuubiFunSuite import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.{REST, THRIFT_BINARY} import org.apache.kyuubi.service.authentication.AuthenticationProviderFactory.getAuthenticationProvider class CustomAuthenticationProviderImplSuite extends KyuubiFunSuite { @@ -28,14 +29,35 @@ class CustomAuthenticationProviderImplSuite extends KyuubiFunSuite { val conf = KyuubiConf() val e1 = intercept[AuthenticationException]( - getAuthenticationProvider(AuthMethods.withName("CUSTOM"), conf)) + getAuthenticationProvider(AuthMethods.withName("CUSTOM"), conf, THRIFT_BINARY)) assert(e1.getMessage.contains( "authentication.custom.class must be set when auth method was CUSTOM.")) conf.set( KyuubiConf.AUTHENTICATION_CUSTOM_CLASS, classOf[UserDefineAuthenticationProviderImpl].getCanonicalName) - val p1 = getAuthenticationProvider(AuthMethods.withName("CUSTOM"), conf) + val p1 = getAuthenticationProvider(AuthMethods.withName("CUSTOM"), conf, THRIFT_BINARY) + val e2 = intercept[AuthenticationException](p1.authenticate("test", "test")) + assert(e2.getMessage.contains("Username or password is not valid!")) + + p1.authenticate("user", "password") + } +} + +class RestCustomAuthenticationProviderImplSuite extends KyuubiFunSuite { + test("Test user defined authentication") { + val conf = KyuubiConf() + + conf.set( + KyuubiConf.FRONTEND_REST_AUTHENTICATION_CUSTOM_CLASS, + Some(classOf[UserDefineAuthenticationProviderImpl].getCanonicalName)) + + val e1 = intercept[AuthenticationException]( + getAuthenticationProvider(AuthMethods.withName("CUSTOM"), conf, THRIFT_BINARY)) + assert(e1.getMessage.contains( + "authentication.custom.class must be set when auth method was CUSTOM.")) + + val p1 = getAuthenticationProvider(AuthMethods.withName("CUSTOM"), conf, REST) val e2 = intercept[AuthenticationException](p1.authenticate("test", "test")) assert(e2.getMessage.contains("Username or password is not valid!")) diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactorySuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactorySuite.scala index 18520b39b64..2b5cb256723 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactorySuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/KyuubiAuthenticationFactorySuite.scala @@ -22,6 +22,7 @@ import javax.security.auth.login.LoginException import org.apache.kyuubi.{KyuubiFunSuite, KyuubiSQLException} import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.THRIFT_BINARY import org.apache.kyuubi.service.authentication.PlainSASLServer.SaslPlainProvider import org.apache.kyuubi.shaded.thrift.transport.TSaslServerTransport import org.apache.kyuubi.util.AssertionUtils._ @@ -46,7 +47,7 @@ class KyuubiAuthenticationFactorySuite extends KyuubiFunSuite { test("AuthType NONE") { val kyuubiConf = KyuubiConf() - val auth = new KyuubiAuthenticationFactory(kyuubiConf) + val auth = new KyuubiAuthenticationFactory(kyuubiConf, THRIFT_BINARY) auth.getTTransportFactory assert(Security.getProviders.exists(_.isInstanceOf[SaslPlainProvider])) @@ -56,14 +57,15 @@ class KyuubiAuthenticationFactorySuite extends KyuubiFunSuite { test("AuthType Other") { val conf = KyuubiConf().set(KyuubiConf.AUTHENTICATION_METHOD, Seq("INVALID")) - interceptEquals[IllegalArgumentException] { new KyuubiAuthenticationFactory(conf) }( - "The value of kyuubi.authentication should be one of" + - " NOSASL, NONE, LDAP, JDBC, KERBEROS, CUSTOM, but was INVALID") + interceptEquals[IllegalArgumentException] { + new KyuubiAuthenticationFactory(conf, THRIFT_BINARY) + }("The value of kyuubi.authentication should be one of" + + " NOSASL, NONE, LDAP, JDBC, KERBEROS, CUSTOM, but was INVALID") } test("AuthType LDAP") { val conf = KyuubiConf().set(KyuubiConf.AUTHENTICATION_METHOD, Seq("LDAP")) - val authFactory = new KyuubiAuthenticationFactory(conf) + val authFactory = new KyuubiAuthenticationFactory(conf, THRIFT_BINARY) authFactory.getTTransportFactory assert(Security.getProviders.exists(_.isInstanceOf[SaslPlainProvider])) } @@ -71,18 +73,18 @@ class KyuubiAuthenticationFactorySuite extends KyuubiFunSuite { test("AuthType KERBEROS w/o keytab/principal") { val conf = KyuubiConf().set(KyuubiConf.AUTHENTICATION_METHOD, Seq("KERBEROS")) - val factory = new KyuubiAuthenticationFactory(conf) + val factory = new KyuubiAuthenticationFactory(conf, THRIFT_BINARY) val e = intercept[LoginException](factory.getTTransportFactory) assert(e.getMessage startsWith "Kerberos principal should have 3 parts") } test("AuthType is NOSASL if only NOSASL is specified") { val conf = KyuubiConf().set(KyuubiConf.AUTHENTICATION_METHOD, Seq("NOSASL")) - var factory = new KyuubiAuthenticationFactory(conf) + var factory = new KyuubiAuthenticationFactory(conf, THRIFT_BINARY) !factory.getTTransportFactory.isInstanceOf[TSaslServerTransport.Factory] conf.set(KyuubiConf.AUTHENTICATION_METHOD, Seq("NOSASL", "NONE")) - factory = new KyuubiAuthenticationFactory(conf) + factory = new KyuubiAuthenticationFactory(conf, THRIFT_BINARY) factory.getTTransportFactory.isInstanceOf[TSaslServerTransport.Factory] } } diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/PlainSASLHelperSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/PlainSASLHelperSuite.scala index 2a55cc0d7ef..7681ac709df 100644 --- a/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/PlainSASLHelperSuite.scala +++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/PlainSASLHelperSuite.scala @@ -21,6 +21,7 @@ import java.security.Security import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiFunSuite} import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.THRIFT_BINARY import org.apache.kyuubi.service.{NoopTBinaryFrontendServer, TBinaryFrontendService} import org.apache.kyuubi.service.authentication.PlainSASLServer.SaslPlainProvider import org.apache.kyuubi.shaded.thrift.transport.{TSaslServerTransport, TSocket} @@ -39,20 +40,20 @@ class PlainSASLHelperSuite extends KyuubiFunSuite { val tProcessor = tProcessorFactory.getProcessor(tSocket) assert(tProcessor.isInstanceOf[TSetIpAddressProcessor[_]]) val e = intercept[IllegalArgumentException] { - PlainSASLHelper.getTransportFactory("KERBEROS", conf) + PlainSASLHelper.getTransportFactory("KERBEROS", conf, THRIFT_BINARY) } assert(e.getMessage === "Illegal authentication type KERBEROS for plain transport") val e2 = intercept[IllegalArgumentException] { - PlainSASLHelper.getTransportFactory("NOSASL", conf) + PlainSASLHelper.getTransportFactory("NOSASL", conf, THRIFT_BINARY) } assert(e2.getMessage === "Illegal authentication type NOSASL for plain transport") val e3 = intercept[IllegalArgumentException] { - PlainSASLHelper.getTransportFactory("ELSE", conf) + PlainSASLHelper.getTransportFactory("ELSE", conf, THRIFT_BINARY) } assert(e3.getMessage === "Illegal authentication type ELSE for plain transport") - val tTransportFactory = PlainSASLHelper.getTransportFactory("NONE", conf) + val tTransportFactory = PlainSASLHelper.getTransportFactory("NONE", conf, THRIFT_BINARY) assert(tTransportFactory.isInstanceOf[TSaslServerTransport.Factory]) Security.getProviders.exists(_.isInstanceOf[SaslPlainProvider]) } diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala index 83aee66fef0..1914e3be140 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala @@ -31,6 +31,7 @@ import org.eclipse.jetty.servlet.{ErrorPageErrorHandler, FilterHolder} import org.apache.kyuubi.{KyuubiException, Utils} import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.REST import org.apache.kyuubi.server.api.v1.ApiRootResource import org.apache.kyuubi.server.http.authentication.{AuthenticationFilter, KyuubiHttpAuthenticationFactory} import org.apache.kyuubi.server.ui.{JettyServer, JettyUtils} @@ -72,7 +73,7 @@ class KyuubiRestFrontendService(override val serverable: Serverable) private lazy val port: Int = conf.get(FRONTEND_REST_BIND_PORT) private[kyuubi] lazy val securityEnabled = { - val authTypes = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName) + val authTypes = conf.get(FRONTEND_REST_AUTHENTICATION_METHOD).map(AuthTypes.withName) AuthUtils.kerberosEnabled(authTypes) || !AuthUtils.effectivePlainAuthType(authTypes).contains(AuthTypes.NONE) } @@ -103,7 +104,7 @@ class KyuubiRestFrontendService(override val serverable: Serverable) private def startInternal(): Unit = { val contextHandler = ApiRootResource.getServletHandler(this) - val holder = new FilterHolder(new AuthenticationFilter(conf)) + val holder = new FilterHolder(new AuthenticationFilter(conf, REST)) contextHandler.addFilter(holder, "/v1/*", EnumSet.allOf(classOf[DispatcherType])) val authenticationFactory = new KyuubiHttpAuthenticationFactory(conf) server.addHandler(authenticationFactory.httpHandlerWrapperFactory.wrapHandler(contextHandler)) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala index 2763e9481f9..0b55260a495 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiTHttpFrontendService.scala @@ -36,11 +36,13 @@ import org.eclipse.jetty.util.thread.ExecutorThreadPool import org.apache.kyuubi.KyuubiException import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf._ +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.THRIFT_HTTP import org.apache.kyuubi.metrics.MetricsConstants.{THRIFT_HTTP_CONN_FAIL, THRIFT_HTTP_CONN_OPEN, THRIFT_HTTP_CONN_TOTAL} import org.apache.kyuubi.metrics.MetricsSystem import org.apache.kyuubi.server.http.ThriftHttpServlet import org.apache.kyuubi.server.http.util.SessionManager import org.apache.kyuubi.service.{Serverable, Service, ServiceUtils, TFrontendService} +import org.apache.kyuubi.service.authentication.KyuubiAuthenticationFactory import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TCLIService, TOpenSessionReq} import org.apache.kyuubi.shaded.thrift.protocol.TBinaryProtocol import org.apache.kyuubi.util.NamedThreadFactory @@ -59,6 +61,8 @@ final class KyuubiTHttpFrontendService( override protected lazy val portNum: Int = conf.get(FRONTEND_THRIFT_HTTP_BIND_PORT) override protected lazy val actualPort: Int = portNum override protected lazy val serverSocket: ServerSocket = null + override protected lazy val authFactory: KyuubiAuthenticationFactory = + new KyuubiAuthenticationFactory(conf, THRIFT_HTTP, isServer()) private var server: Option[Server] = None private val APPLICATION_THRIFT = "application/x-thrift" diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala index 980f35d70b5..232eca1bc8e 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala @@ -31,6 +31,7 @@ import org.apache.hadoop.hive.shims.Utils import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.config.KyuubiConf.FRONTEND_PROXY_HTTP_CLIENT_IP_HEADER +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.THRIFT_HTTP import org.apache.kyuubi.server.http.authentication.AuthenticationFilter import org.apache.kyuubi.server.http.util.{CookieSigner, HttpAuthUtils, SessionManager} import org.apache.kyuubi.server.http.util.HttpAuthUtils.AUTHORIZATION_HEADER @@ -56,7 +57,7 @@ class ThriftHttpServlet( private var isCookieSecure = false private var isHttpOnlyCookie = false private val X_FORWARDED_FOR_HEADER = "X-Forwarded-For" - private val authenticationFilter = new AuthenticationFilter(conf) + private val authenticationFilter = new AuthenticationFilter(conf, THRIFT_HTTP) override def init(): Unit = { isCookieAuthEnabled = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_COOKIE_AUTH_ENABLED) diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala index 15b387607ea..b271cedf220 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala @@ -26,12 +26,14 @@ import scala.collection.mutable import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.config.KyuubiConf.{AUTHENTICATION_METHOD, FRONTEND_PROXY_HTTP_CLIENT_IP_HEADER} +import org.apache.kyuubi.config.KyuubiConf.{AUTHENTICATION_METHOD, FRONTEND_PROXY_HTTP_CLIENT_IP_HEADER, FRONTEND_REST_AUTHENTICATION_METHOD} +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.{FrontendProtocol, REST} import org.apache.kyuubi.server.http.util.HttpAuthUtils.AUTHORIZATION_HEADER import org.apache.kyuubi.service.authentication.{AuthTypes, InternalSecurityAccessor} import org.apache.kyuubi.service.authentication.AuthTypes.{KERBEROS, NOSASL} -class AuthenticationFilter(conf: KyuubiConf) extends Filter with Logging { +class AuthenticationFilter(conf: KyuubiConf, protocol: FrontendProtocol) extends Filter + with Logging { import AuthenticationFilter._ import AuthSchemes._ @@ -55,7 +57,10 @@ class AuthenticationFilter(conf: KyuubiConf) extends Filter with Logging { } private[kyuubi] def initAuthHandlers(): Unit = { - val authTypes = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName) + val authTypes = protocol match { + case REST => conf.get(FRONTEND_REST_AUTHENTICATION_METHOD).map(AuthTypes.withName) + case _ => conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName) + } val spnegoKerberosEnabled = authTypes.contains(KERBEROS) val basicAuthTypeOpt = { if (authTypes == Set(NOSASL)) { @@ -69,7 +74,7 @@ class AuthenticationFilter(conf: KyuubiConf) extends Filter with Logging { addAuthHandler(kerberosHandler) } basicAuthTypeOpt.foreach { basicAuthType => - val basicHandler = new BasicAuthenticationHandler(basicAuthType) + val basicHandler = new BasicAuthenticationHandler(basicAuthType, protocol) addAuthHandler(basicHandler) } if (InternalSecurityAccessor.get() != null) { diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BasicAuthenticationHandler.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BasicAuthenticationHandler.scala index 76560cabb55..231cb5248a9 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BasicAuthenticationHandler.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BasicAuthenticationHandler.scala @@ -23,12 +23,13 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.apache.kyuubi.Logging import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.FrontendProtocol import org.apache.kyuubi.server.http.authentication.AuthSchemes.AuthScheme import org.apache.kyuubi.server.http.util.HttpAuthUtils.{AUTHORIZATION_HEADER, WWW_AUTHENTICATE_HEADER} import org.apache.kyuubi.service.authentication.{AuthenticationProviderFactory, AuthMethods} import org.apache.kyuubi.service.authentication.AuthTypes._ -class BasicAuthenticationHandler(basicAuthType: AuthType) +class BasicAuthenticationHandler(basicAuthType: AuthType, protocol: FrontendProtocol) extends AuthenticationHandler with Logging { private var conf: KyuubiConf = _ @@ -80,7 +81,7 @@ class BasicAuthenticationHandler(basicAuthType: AuthType) } else { val Seq(user, password) = creds.toSeq.take(2) val passwdAuthenticationProvider = AuthenticationProviderFactory - .getAuthenticationProvider(AuthMethods.withName(basicAuthType.toString), conf) + .getAuthenticationProvider(AuthMethods.withName(basicAuthType.toString), conf, protocol) passwdAuthenticationProvider.authenticate(user, password) response.setStatus(HttpServletResponse.SC_OK) authUser = user diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KyuubiHttpAuthenticationFactory.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KyuubiHttpAuthenticationFactory.scala index ca95fda3d9a..250ee5d6462 100644 --- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KyuubiHttpAuthenticationFactory.scala +++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/KyuubiHttpAuthenticationFactory.scala @@ -25,14 +25,14 @@ import org.eclipse.jetty.server.{Handler, Request} import org.eclipse.jetty.server.handler.HandlerWrapper import org.apache.kyuubi.config.KyuubiConf -import org.apache.kyuubi.config.KyuubiConf.{AUTHENTICATION_METHOD, ENGINE_SECURITY_ENABLED} +import org.apache.kyuubi.config.KyuubiConf.{ENGINE_SECURITY_ENABLED, FRONTEND_REST_AUTHENTICATION_METHOD} import org.apache.kyuubi.metrics.MetricsConstants.{REST_CONN_FAIL, REST_CONN_OPEN, REST_CONN_TOTAL} import org.apache.kyuubi.metrics.MetricsSystem import org.apache.kyuubi.service.authentication.{AuthTypes, InternalSecurityAccessor} import org.apache.kyuubi.service.authentication.AuthTypes.KERBEROS class KyuubiHttpAuthenticationFactory(conf: KyuubiConf) { - private val authTypes = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName) + private val authTypes = conf.get(FRONTEND_REST_AUTHENTICATION_METHOD).map(AuthTypes.withName) private val kerberosEnabled = authTypes.contains(KERBEROS) private val ugi = UserGroupInformation.getCurrentUser diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiRestAuthenticationSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiRestAuthenticationSuite.scala index 260264b6797..0464d0d0f03 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiRestAuthenticationSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiRestAuthenticationSuite.scala @@ -24,6 +24,7 @@ import javax.ws.rs.core.MediaType import scala.collection.JavaConverters._ +import org.apache.hadoop.conf.Configuration import org.apache.hadoop.security.UserGroupInformation import org.apache.kyuubi.RestClientTestHelper @@ -31,7 +32,7 @@ import org.apache.kyuubi.client.api.v1.dto.{SessionHandle, SessionOpenCount, Ses import org.apache.kyuubi.config.KyuubiConf import org.apache.kyuubi.server.http.authentication.AuthSchemes import org.apache.kyuubi.server.http.util.HttpAuthUtils._ -import org.apache.kyuubi.service.authentication.InternalSecurityAccessor +import org.apache.kyuubi.service.authentication.{InternalSecurityAccessor, UserDefineAuthenticationProviderImpl} import org.apache.kyuubi.session.KyuubiSession class KyuubiRestAuthenticationSuite extends RestClientTestHelper { @@ -182,3 +183,89 @@ class KyuubiRestAuthenticationSuite extends RestClientTestHelper { assert(HttpServletResponse.SC_UNAUTHORIZED == response.getStatus) } } + +class KyuubiRestIsolatedNoneAuthenticationSuite extends RestClientTestHelper { + + override protected val otherConfigs: Map[String, String] = { + Map( + KyuubiConf.AUTHENTICATION_METHOD.key -> "KERBEROS", + KyuubiConf.FRONTEND_REST_AUTHENTICATION_METHOD.key -> "NONE") + } + + test("test disable restful api authentication") { + val response = webTarget.path("api/v1/sessions/count") + .request() + .get() + + assert(HttpServletResponse.SC_OK == response.getStatus) + } +} + +class KyuubiRestKerberosAuthenticationSuite extends RestClientTestHelper { + + override protected val otherConfigs: Map[String, String] = { + Map(KyuubiConf.AUTHENTICATION_METHOD.key -> "KERBEROS") + } + + test("test without authorization when rest api authentication not isolated with KERBEROS") { + val response = webTarget.path("api/v1/sessions/count") + .request() + .get() + + assert(HttpServletResponse.SC_UNAUTHORIZED == response.getStatus) + } +} + +class KyuubiRestIsolatedKerberosAuthenticationSuite extends RestClientTestHelper { + + override protected val otherConfigs: Map[String, String] = { + Map( + KyuubiConf.AUTHENTICATION_METHOD.key -> "NONE", + KyuubiConf.FRONTEND_REST_AUTHENTICATION_METHOD.key -> "KERBEROS") + } + + test("test isolated kerberos restful api authentication") { + val response = webTarget.path("api/v1/sessions/count") + .request() + .get() + + assert(HttpServletResponse.SC_UNAUTHORIZED == response.getStatus) + } +} + +class KyuubiRestIsolatedAuthenticationSuite extends KyuubiRestAuthenticationSuite { + + override protected val otherConfigs: Map[String, String] = { + Map( + KyuubiConf.ENGINE_SECURITY_ENABLED.key -> "true", + KyuubiConf.ENGINE_SECURITY_SECRET_PROVIDER.key -> "simple", + KyuubiConf.SIMPLE_SECURITY_SECRET_PROVIDER_PROVIDER_SECRET.key -> "_KYUUBI_REST_", + // allow to impersonate other users with spnego authentication + s"hadoop.proxyuser.$clientPrincipalUser.groups" -> "*", + s"hadoop.proxyuser.$clientPrincipalUser.hosts" -> "*") + } + + override protected lazy val conf: KyuubiConf = { + val config = new Configuration() + val authType = "hadoop.security.authentication" + config.set(authType, "KERBEROS") + System.setProperty("java.security.krb5.conf", krb5ConfPath) + UserGroupInformation.setConfiguration(config) + assert(UserGroupInformation.isSecurityEnabled) + + val conf = KyuubiConf().set( + KyuubiConf.FRONTEND_REST_AUTHENTICATION_METHOD, + Seq("KERBEROS", "LDAP", "CUSTOM")) + .set(KyuubiConf.SERVER_KEYTAB.key, testKeytab) + .set(KyuubiConf.SERVER_PRINCIPAL, testPrincipal) + .set(KyuubiConf.SERVER_SPNEGO_KEYTAB, testKeytab) + .set(KyuubiConf.SERVER_SPNEGO_PRINCIPAL, testSpnegoPrincipal) + .set(KyuubiConf.AUTHENTICATION_LDAP_URL, ldapUrl) + .set(KyuubiConf.AUTHENTICATION_LDAP_BASE_DN, ldapBaseDn.head) + .set( + KyuubiConf.FRONTEND_REST_AUTHENTICATION_CUSTOM_CLASS, + Some(classOf[UserDefineAuthenticationProviderImpl].getCanonicalName)) + otherConfigs.foreach(kv => conf.set(kv._1, kv._2)) + conf + } +} diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilterSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/RestAuthenticationFilterSuite.scala similarity index 83% rename from kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilterSuite.scala rename to kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/RestAuthenticationFilterSuite.scala index de4b056ff46..ff2a98e63fc 100644 --- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilterSuite.scala +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/RestAuthenticationFilterSuite.scala @@ -19,16 +19,17 @@ package org.apache.kyuubi.server.http.authentication import org.apache.kyuubi.KyuubiFunSuite import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.REST import org.apache.kyuubi.service.authentication.AuthTypes -class AuthenticationFilterSuite extends KyuubiFunSuite { +class RestAuthenticationFilterSuite extends KyuubiFunSuite { test("add auth handler and destroy") { - val filter = new AuthenticationFilter(KyuubiConf()) - filter.addAuthHandler(new BasicAuthenticationHandler(null)) + val filter = new AuthenticationFilter(KyuubiConf(), REST) + filter.addAuthHandler(new BasicAuthenticationHandler(null, REST)) assert(filter.authSchemeHandlers.isEmpty) - filter.addAuthHandler(new BasicAuthenticationHandler(AuthTypes.LDAP)) + filter.addAuthHandler(new BasicAuthenticationHandler(AuthTypes.LDAP, REST)) assert(filter.authSchemeHandlers.size == 1) - filter.addAuthHandler(new BasicAuthenticationHandler(AuthTypes.LDAP)) + filter.addAuthHandler(new BasicAuthenticationHandler(AuthTypes.LDAP, REST)) assert(filter.authSchemeHandlers.size == 1) filter.addAuthHandler(new KerberosAuthenticationHandler()) assert(filter.authSchemeHandlers.size == 1) diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/ThriftHttpAuthenticationFilterSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/ThriftHttpAuthenticationFilterSuite.scala new file mode 100644 index 00000000000..0dce840ca47 --- /dev/null +++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/ThriftHttpAuthenticationFilterSuite.scala @@ -0,0 +1,39 @@ +/* + * 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.server.http.authentication + +import org.apache.kyuubi.KyuubiFunSuite +import org.apache.kyuubi.config.KyuubiConf +import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols.THRIFT_HTTP +import org.apache.kyuubi.service.authentication.AuthTypes + +class ThriftHttpAuthenticationFilterSuite extends KyuubiFunSuite { + test("add auth handler and destroy") { + val filter = new AuthenticationFilter(KyuubiConf(), THRIFT_HTTP) + filter.addAuthHandler(new BasicAuthenticationHandler(null, THRIFT_HTTP)) + assert(filter.authSchemeHandlers.isEmpty) + filter.addAuthHandler(new BasicAuthenticationHandler(AuthTypes.LDAP, THRIFT_HTTP)) + assert(filter.authSchemeHandlers.size == 1) + filter.addAuthHandler(new BasicAuthenticationHandler(AuthTypes.LDAP, THRIFT_HTTP)) + assert(filter.authSchemeHandlers.size == 1) + filter.addAuthHandler(new KerberosAuthenticationHandler()) + assert(filter.authSchemeHandlers.size == 1) + filter.destroy() + assert(filter.authSchemeHandlers.isEmpty) + } +}