[KYUUBI #2719] [SUB-TASK][KPIP-4] Support internal rest request authentication to enable http request redirection across kyuubi instances

### _Why are the changes needed?_

Support internal rest request authentication to enable redirect http request across kyuubi instances

### _How was this patch tested?_
- [x] Add some test cases that check the changes thoroughly including negative and positive cases if possible

- [ ] Add screenshots for manual tests if appropriate

- [x] [Run test](https://kyuubi.apache.org/docs/latest/develop_tools/testing.html#running-tests) locally before make a pull request

Closes #2719 from turboFei/internal_access.

Closes #2719

b9e150e9 [Fei Wang] revert config key change
aa378727 [Fei Wang] make it internal
ce96d922 [Fei Wang] comments
6fe8523b [Fei Wang] Support internal rest request authentication to enable redirect http request across kyuubi instances

Authored-by: Fei Wang <fwang12@ebay.com>
Signed-off-by: Fei Wang <fwang12@ebay.com>
This commit is contained in:
Fei Wang 2022-05-23 15:44:48 +08:00
parent 957847510f
commit f1cf95fe96
14 changed files with 152 additions and 37 deletions

View File

@ -220,13 +220,6 @@ Key | Default | Meaning | Type | Since
<code>kyuubi.engine.pool.name</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>engine-pool</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The name of engine pool.</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.engine.pool.size</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>-1</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The size of engine pool. Note that, if the size is less than 1, the engine pool will not be enabled; otherwise, the size of the engine pool will be min(this, kyuubi.engine.pool.size.threshold).</div>|<div style='width: 30pt'>int</div>|<div style='width: 20pt'>1.4.0</div>
<code>kyuubi.engine.pool.size.threshold</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>9</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>This parameter is introduced as a server-side parameter, and controls the upper limit of the engine pool.</div>|<div style='width: 30pt'>int</div>|<div style='width: 20pt'>1.4.0</div>
<code>kyuubi.engine.security.crypto.cipher</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>AES/CBC/PKCS5PADDING</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The cipher transformation to use for encrypting engine access token.</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.engine.security.crypto.ivLength</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>16</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>Initial vector length, in bytes.</div>|<div style='width: 30pt'>int</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.engine.security.crypto.keyAlgorithm</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>AES</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The algorithm for generated secret keys.</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.engine.security.crypto.keyLength</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>128</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The length in bits of the encryption key to generate. Valid values are 128, 192 and 256</div>|<div style='width: 30pt'>int</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.engine.security.enabled</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>false</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>Whether to enable the internal secure access between Kyuubi server and engine.</div>|<div style='width: 30pt'>boolean</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.engine.security.secret.provider</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>org.apache.kyuubi.service.authentication.ZooKeeperEngineSecuritySecretProviderImpl</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The class used to manage the engine security secret. This class must be a subclass of EngineSecuritySecretProvider.</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.engine.security.token.max.lifetime</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>PT10M</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The max lifetime of the token used for secure access between Kyuubi server and engine.</div>|<div style='width: 30pt'>duration</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.engine.session.initialize.sql</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'></div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>SemiColon-separated list of SQL statements to be initialized in the newly created engine session before queries. This configuration can not be used in JDBC url due to the limitation of Beeline/JDBC driver.</div>|<div style='width: 30pt'>seq</div>|<div style='width: 20pt'>1.3.0</div>
<code>kyuubi.engine.share.level</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>USER</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>Engines will be shared in different levels, available configs are: <ul> <li>CONNECTION: engine will not be shared but only used by the current client connection</li> <li>USER: engine will be shared by all sessions created by a unique username, see also kyuubi.engine.share.level.subdomain</li> <li>GROUP: engine will be shared by all sessions created by all users belong to the same primary group name. The engine will be launched by the group name as the effective username, so here the group name is kind of special user who is able to visit the compute resources/data of a team. It follows the [Hadoop GroupsMapping](https://reurl.cc/xE61Y5) to map user to a primary group. If the primary group is not found, it fallback to the USER level. <li>SERVER: the App will be shared by Kyuubi servers</li></ul></div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.2.0</div>
<code>kyuubi.engine.share.level.sub.domain</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>&lt;undefined&gt;</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>(deprecated) - Using kyuubi.engine.share.level.subdomain instead</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.2.0</div>
@ -292,7 +285,6 @@ Key | Default | Meaning | Type | Since
<code>kyuubi.ha.zookeeper.connection.retry.policy</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>EXPONENTIAL_BACKOFF</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The retry policy for connecting to the zookeeper ensemble, all candidates are: <ul><li>ONE_TIME</li><li> N_TIME</li><li> EXPONENTIAL_BACKOFF</li><li> BOUNDED_EXPONENTIAL_BACKOFF</li><li> UNTIL_ELAPSED</li></ul></div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.0.0</div>
<code>kyuubi.ha.zookeeper.connection.timeout</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>15000</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The timeout(ms) of creating the connection to the zookeeper ensemble</div>|<div style='width: 30pt'>int</div>|<div style='width: 20pt'>1.0.0</div>
<code>kyuubi.ha.zookeeper.engine.auth.type</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>NONE</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The type of zookeeper authentication for engine, all candidates are <ul><li>NONE</li><li> KERBEROS</li><li> DIGEST</li></ul></div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.3.2</div>
<code>kyuubi.ha.zookeeper.engine.secure.secret.node</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>&lt;undefined&gt;</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The zk node contains the secret that used for internal secure between Kyuubi server and Kyuubi engine, please make sure that it is only visible for Kyuubi.</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.5.0</div>
<code>kyuubi.ha.zookeeper.namespace</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>kyuubi</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>The root directory for the service to deploy its instance uri</div>|<div style='width: 30pt'>string</div>|<div style='width: 20pt'>1.0.0</div>
<code>kyuubi.ha.zookeeper.node.creation.timeout</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>PT2M</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>Timeout for creating zookeeper node</div>|<div style='width: 30pt'>duration</div>|<div style='width: 20pt'>1.2.0</div>
<code>kyuubi.ha.zookeeper.publish.configs</code>|<div style='width: 65pt;word-wrap: break-word;white-space: normal'>false</div>|<div style='width: 170pt;word-wrap: break-word;white-space: normal'>When set to true, publish Kerberos configs to Zookeeper.Note that the Hive driver needs to be greater than 1.3 or 2.0 or apply HIVE-11581 patch.</div>|<div style='width: 30pt'>boolean</div>|<div style='width: 20pt'>1.4.0</div>

View File

@ -1250,21 +1250,26 @@ object KyuubiConf {
val ENGINE_SECURITY_ENABLED: ConfigEntry[Boolean] =
buildConf("kyuubi.engine.security.enabled")
.doc("Whether to enable the internal secure access between Kyuubi server and engine.")
.internal
.doc("Whether to enable the internal secure access. Before 1.6.0, it is used for the secure" +
" access between kyuubi server and kyuubi engine. Since 1.6.0, kyuubi supports internal" +
" secure across kyuubi server instances.")
.version("1.5.0")
.booleanConf
.createWithDefault(false)
val ENGINE_SECURITY_TOKEN_MAX_LIFETIME: ConfigEntry[Long] =
buildConf("kyuubi.engine.security.token.max.lifetime")
.doc("The max lifetime of the token used for secure access between Kyuubi server and engine.")
.internal
.doc("The max lifetime of the token used for internal secure access.")
.version("1.5.0")
.timeConf
.createWithDefault(Duration.ofMinutes(10).toMillis)
val ENGINE_SECURITY_SECRET_PROVIDER: ConfigEntry[String] =
buildConf("kyuubi.engine.security.secret.provider")
.doc("The class used to manage the engine security secret. This class must be a " +
.internal
.doc("The class used to manage the internal security secret. This class must be a " +
"subclass of EngineSecuritySecretProvider.")
.version("1.5.0")
.stringConf
@ -1273,6 +1278,7 @@ object KyuubiConf {
val ENGINE_SECURITY_CRYPTO_KEY_LENGTH: ConfigEntry[Int] =
buildConf("kyuubi.engine.security.crypto.keyLength")
.internal
.doc("The length in bits of the encryption key to generate. " +
"Valid values are 128, 192 and 256")
.version("1.5.0")
@ -1282,6 +1288,7 @@ object KyuubiConf {
val ENGINE_SECURITY_CRYPTO_IV_LENGTH: ConfigEntry[Int] =
buildConf("kyuubi.engine.security.crypto.ivLength")
.internal
.doc("Initial vector length, in bytes.")
.version("1.5.0")
.intConf
@ -1289,6 +1296,7 @@ object KyuubiConf {
val ENGINE_SECURITY_CRYPTO_KEY_ALGORITHM: ConfigEntry[String] =
buildConf("kyuubi.engine.security.crypto.keyAlgorithm")
.internal
.doc("The algorithm for generated secret keys.")
.version("1.5.0")
.stringConf
@ -1296,7 +1304,8 @@ object KyuubiConf {
val ENGINE_SECURITY_CRYPTO_CIPHER_TRANSFORMATION: ConfigEntry[String] =
buildConf("kyuubi.engine.security.crypto.cipher")
.doc("The cipher transformation to use for encrypting engine access token.")
.internal
.doc("The cipher transformation to use for encrypting internal access token.")
.version("1.5.0")
.stringConf
.createWithDefault("AES/CBC/PKCS5PADDING")

View File

@ -19,6 +19,6 @@ package org.apache.kyuubi.service.authentication
class EngineSecureAuthenticationProviderImpl extends PasswdAuthenticationProvider {
override def authenticate(user: String, password: String): Unit = {
EngineSecurityAccessor.get().authToken(password)
InternalSecurityAccessor.get().authToken(password)
}
}

View File

@ -24,7 +24,7 @@ import org.apache.kyuubi.{KyuubiSQLException, Logging}
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.config.KyuubiConf._
class EngineSecurityAccessor(conf: KyuubiConf, val isServer: Boolean) {
class InternalSecurityAccessor(conf: KyuubiConf, val isServer: Boolean) {
val cryptoKeyLengthBytes = conf.get(ENGINE_SECURITY_CRYPTO_KEY_LENGTH) / java.lang.Byte.SIZE
val cryptoIvLength = conf.get(ENGINE_SECURITY_CRYPTO_IV_LENGTH)
val cryptoKeyAlgorithm = conf.get(ENGINE_SECURITY_CRYPTO_KEY_ALGORITHM)
@ -109,16 +109,16 @@ class EngineSecurityAccessor(conf: KyuubiConf, val isServer: Boolean) {
}
}
object EngineSecurityAccessor extends Logging {
@volatile private var _engineSecurityAccessor: EngineSecurityAccessor = _
object InternalSecurityAccessor extends Logging {
@volatile private var _engineSecurityAccessor: InternalSecurityAccessor = _
def initialize(conf: KyuubiConf, isServer: Boolean): Unit = {
if (_engineSecurityAccessor == null) {
_engineSecurityAccessor = new EngineSecurityAccessor(conf, isServer)
_engineSecurityAccessor = new InternalSecurityAccessor(conf, isServer)
}
}
def get(): EngineSecurityAccessor = {
def get(): InternalSecurityAccessor = {
_engineSecurityAccessor
}
}

View File

@ -57,7 +57,7 @@ class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) ex
}
if (conf.get(ENGINE_SECURITY_ENABLED)) {
EngineSecurityAccessor.initialize(conf, isServer)
InternalSecurityAccessor.initialize(conf, isServer)
}
private def getSaslProperties: java.util.Map[String, String] = {

View File

@ -20,7 +20,7 @@ package org.apache.kyuubi.service.authentication
import org.apache.kyuubi.{KyuubiFunSuite, KyuubiSQLException}
import org.apache.kyuubi.config.KyuubiConf
class EngineSecurityAccessorSuite extends KyuubiFunSuite {
class InternalSecurityAccessorSuite extends KyuubiFunSuite {
private val conf = KyuubiConf()
conf.set(
KyuubiConf.ENGINE_SECURITY_SECRET_PROVIDER,
@ -31,7 +31,7 @@ class EngineSecurityAccessorSuite extends KyuubiFunSuite {
val newConf = conf.clone
newConf.set(KyuubiConf.ENGINE_SECURITY_CRYPTO_CIPHER_TRANSFORMATION, cipher)
val secureAccessor = new EngineSecurityAccessor(newConf, true)
val secureAccessor = new InternalSecurityAccessor(newConf, true)
val value = "tokenToEncrypt"
val encryptedValue = secureAccessor.encrypt(value)
assert(secureAccessor.decrypt(encryptedValue) === value)
@ -40,7 +40,7 @@ class EngineSecurityAccessorSuite extends KyuubiFunSuite {
secureAccessor.authToken(token)
intercept[KyuubiSQLException](secureAccessor.authToken("invalidToken"))
val engineSecureAccessor = new EngineSecurityAccessor(newConf, false)
val engineSecureAccessor = new InternalSecurityAccessor(newConf, false)
engineSecureAccessor.authToken(token)
}
}

View File

@ -153,8 +153,9 @@ object HighAvailabilityConf {
val HA_ZK_ENGINE_SECURE_SECRET_NODE: OptionalConfigEntry[String] =
buildConf("kyuubi.ha.zookeeper.engine.secure.secret.node")
.doc("The zk node contains the secret that used for internal secure between Kyuubi server " +
"and Kyuubi engine, please make sure that it is only visible for Kyuubi.")
.internal
.doc("The zk node contains the secret that used for internal secure, please make sure " +
"that it is only visible for Kyuubi.")
.version("1.5.0")
.stringConf
.createOptional

View File

@ -20,5 +20,5 @@ package org.apache.kyuubi.server.http.authentication
object AuthSchemes extends Enumeration {
type AuthScheme = Value
val BASIC, NEGOTIATE = Value
val BASIC, NEGOTIATE, KYUUBI_INTERNAL = Value
}

View File

@ -28,7 +28,7 @@ import org.apache.hadoop.security.authentication.client.AuthenticationException
import org.apache.kyuubi.Logging
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.config.KyuubiConf.AUTHENTICATION_METHOD
import org.apache.kyuubi.service.authentication.AuthTypes
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 {
@ -72,6 +72,10 @@ class AuthenticationFilter(conf: KyuubiConf) extends Filter with Logging {
val basicHandler = new BasicAuthenticationHandler(basicAuthType)
addAuthHandler(basicHandler)
}
if (InternalSecurityAccessor.get() != null) {
val internalHandler = new KyuubiInternalAuthenticationHandler
addAuthHandler(internalHandler)
}
super.init(filterConfig)
}

View File

@ -26,7 +26,7 @@ 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.service.authentication.{AuthTypes, EngineSecurityAccessor}
import org.apache.kyuubi.service.authentication.{AuthTypes, InternalSecurityAccessor}
import org.apache.kyuubi.service.authentication.AuthTypes.KERBEROS
class KyuubiHttpAuthenticationFactory(conf: KyuubiConf) {
@ -35,7 +35,7 @@ class KyuubiHttpAuthenticationFactory(conf: KyuubiConf) {
private val ugi = UserGroupInformation.getCurrentUser
if (conf.get(ENGINE_SECURITY_ENABLED)) {
EngineSecurityAccessor.initialize(conf, true)
InternalSecurityAccessor.initialize(conf, true)
}
private[kyuubi] val httpHandlerWrapperFactory =

View File

@ -0,0 +1,66 @@
/*
* 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 java.nio.charset.Charset
import java.util.Base64
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import org.apache.kyuubi.Logging
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.server.http.authentication.AuthSchemes.AuthScheme
import org.apache.kyuubi.service.authentication.InternalSecurityAccessor
class KyuubiInternalAuthenticationHandler extends AuthenticationHandler with Logging {
import AuthenticationHandler._
private var conf: KyuubiConf = _
override val authScheme: AuthScheme = AuthSchemes.KYUUBI_INTERNAL
private val internalSecurityAccessor = InternalSecurityAccessor.get()
override def init(conf: KyuubiConf): Unit = {
this.conf = conf
}
override def authenticationSupported: Boolean = {
internalSecurityAccessor != null
}
override def authenticate(
request: HttpServletRequest,
response: HttpServletResponse): String = {
var authUser: String = null
val authorization = getAuthorization(request)
val inputToken = Option(authorization).map(a => Base64.getDecoder.decode(a.getBytes()))
.getOrElse(Array.empty[Byte])
val creds = new String(inputToken, Charset.forName("UTF-8")).split(":")
if (creds.size < 2 || creds(0).trim.isEmpty || creds(1).trim.isEmpty) {
response.setHeader(WWW_AUTHENTICATE, authScheme.toString)
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
} else {
val Seq(user, password) = creds.toSeq.take(2)
internalSecurityAccessor.authToken(password)
response.setStatus(HttpServletResponse.SC_OK)
authUser = user
}
authUser
}
override def destroy(): Unit = {}
}

View File

@ -33,7 +33,7 @@ import org.apache.kyuubi.metrics.MetricsConstants._
import org.apache.kyuubi.metrics.MetricsSystem
import org.apache.kyuubi.operation.{Operation, OperationHandle}
import org.apache.kyuubi.operation.log.OperationLog
import org.apache.kyuubi.service.authentication.EngineSecurityAccessor
import org.apache.kyuubi.service.authentication.InternalSecurityAccessor
class KyuubiSessionImpl(
protocol: TProtocolVersion,
@ -98,7 +98,7 @@ class KyuubiSessionImpl(
val (host, port) = engine.getOrCreate(discoveryClient, extraEngineLog)
val passwd =
if (sessionManager.getConf.get(ENGINE_SECURITY_ENABLED)) {
EngineSecurityAccessor.get().issueToken()
InternalSecurityAccessor.get().issueToken()
} else {
Option(password).filter(_.nonEmpty).getOrElse("anonymous")
}

View File

@ -21,7 +21,7 @@ import org.apache.kyuubi.WithKyuubiServer
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.ha.HighAvailabilityConf
import org.apache.kyuubi.ha.client.DiscoveryClientProvider
import org.apache.kyuubi.service.authentication.{EngineSecurityAccessor, ZooKeeperEngineSecuritySecretProviderImpl}
import org.apache.kyuubi.service.authentication.{InternalSecurityAccessor, ZooKeeperEngineSecuritySecretProviderImpl}
class KyuubiOperationWithEngineSecurity extends WithKyuubiServer with HiveJDBCTestHelper {
import DiscoveryClientProvider._
@ -47,7 +47,7 @@ class KyuubiOperationWithEngineSecurity extends WithKyuubiServer with HiveJDBCTe
}
conf.set(KyuubiConf.ENGINE_SECURITY_ENABLED, true)
EngineSecurityAccessor.initialize(conf, true)
InternalSecurityAccessor.initialize(conf, true)
}
test("engine security") {

View File

@ -29,10 +29,11 @@ import org.apache.hadoop.security.UserGroupInformation
import org.apache.hive.service.rpc.thrift.TProtocolVersion
import org.apache.kyuubi.{KerberizedTestHelper, RestFrontendTestHelper}
import org.apache.kyuubi.client.api.v1.dto.{SessionOpenCount, SessionOpenRequest}
import org.apache.kyuubi.client.api.v1.dto.{SessionHandle, SessionOpenCount, SessionOpenRequest}
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.server.http.authentication.AuthenticationHandler.AUTHORIZATION_HEADER
import org.apache.kyuubi.service.authentication.{UserDefineAuthenticationProviderImpl, WithLdapServer}
import org.apache.kyuubi.server.http.authentication.AuthSchemes
import org.apache.kyuubi.service.authentication._
class KyuubiRestAuthenticationSuite extends RestFrontendTestHelper with KerberizedTestHelper
with WithLdapServer {
@ -71,6 +72,10 @@ class KyuubiRestAuthenticationSuite extends RestFrontendTestHelper with Kerberiz
.set(
KyuubiConf.AUTHENTICATION_CUSTOM_CLASS,
classOf[UserDefineAuthenticationProviderImpl].getCanonicalName)
.set(KyuubiConf.ENGINE_SECURITY_ENABLED, true)
.set(
KyuubiConf.ENGINE_SECURITY_SECRET_PROVIDER,
classOf[UserDefinedEngineSecuritySecretProvider].getCanonicalName)
}
test("test with LDAP authorization") {
@ -156,19 +161,57 @@ class KyuubiRestAuthenticationSuite extends RestFrontendTestHelper with Kerberiz
test("test with ugi wrapped open session") {
UserGroupInformation.loginUserFromKeytab(testPrincipal, testKeytab)
val token = generateToken(hostName)
var token = generateToken(hostName)
val sessionOpenRequest = new SessionOpenRequest(
TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V11.getValue,
"kyuubi",
"pass",
"localhost",
Map.empty[String, String].asJava)
Map(KyuubiConf.ENGINE_SHARE_LEVEL.key -> "CONNECTION").asJava)
val response = webTarget.path("api/v1/sessions")
var response = webTarget.path("api/v1/sessions")
.request()
.header(AUTHORIZATION_HEADER, s"NEGOTIATE $token")
.post(Entity.entity(sessionOpenRequest, MediaType.APPLICATION_JSON_TYPE))
assert(HttpServletResponse.SC_OK == response.getStatus)
val sessionHandle = response.readEntity(classOf[SessionHandle])
token = generateToken(hostName)
val serializedSessionHandle = s"${sessionHandle.getPublicId}|" +
s"${sessionHandle.getSecretId}|${sessionHandle.getProtocolVersion}"
response = webTarget.path(s"api/v1/sessions/$serializedSessionHandle")
.request()
.header(AUTHORIZATION_HEADER, s"NEGOTIATE $token")
.delete()
assert(HttpServletResponse.SC_OK == response.getStatus)
}
test("test with internal authorization") {
val internalSecurityAccessor = InternalSecurityAccessor.get()
var encodeAuthorization = new String(
Base64.getEncoder.encode(
s"$ldapUser:${internalSecurityAccessor.issueToken()}".getBytes()),
"UTF-8")
var response = webTarget.path("api/v1/sessions/count")
.request()
.header(AUTHORIZATION_HEADER, s"${AuthSchemes.KYUUBI_INTERNAL.toString} $encodeAuthorization")
.get()
assert(HttpServletResponse.SC_OK == response.getStatus)
val openedSessionCount = response.readEntity(classOf[SessionOpenCount])
assert(openedSessionCount.getOpenSessionCount == 0)
val badAuthorization = new String(
Base64.getEncoder.encode(
s"$ldapUser:".getBytes()),
"UTF-8")
response = webTarget.path("api/v1/sessions/count")
.request()
.header(AUTHORIZATION_HEADER, s"${AuthSchemes.KYUUBI_INTERNAL.toString} $badAuthorization")
.get()
assert(HttpServletResponse.SC_UNAUTHORIZED == response.getStatus)
}
}