Skip to content

Commit ceb66bd

Browse files
committed
[KYUUBI #2935] Support spnego authentication for thrift http transport mode
### _Why are the changes needed?_ Reuse the AuthenticationFilter for KyuubiTHttpFrontendService Close #2907 ### _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 #2935 from turboFei/http_servelet. Closes #2935 26ba8e7 [Fei Wang] comments 241c047 [Fei Wang] refactor 7de5ffb [Fei Wang] fix 030d5ce [Fei Wang] save f82fbb0 [Fei Wang] save 0850754 [Fei Wang] save Authored-by: Fei Wang <fwang12@ebay.com> Signed-off-by: Fei Wang <fwang12@ebay.com>
1 parent 37c0d42 commit ceb66bd

File tree

5 files changed

+54
-121
lines changed

5 files changed

+54
-121
lines changed

kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/ThriftHttpServlet.scala

Lines changed: 12 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,22 @@ import java.io.IOException
2121
import java.security.SecureRandom
2222
import javax.security.sasl.AuthenticationException
2323
import javax.servlet.ServletException
24-
import javax.servlet.http.Cookie
25-
import javax.servlet.http.HttpServletRequest
26-
import javax.servlet.http.HttpServletResponse
24+
import javax.servlet.http.{Cookie, HttpServletRequest, HttpServletResponse}
2725
import javax.ws.rs.core.NewCookie
2826

2927
import scala.collection.mutable
3028

31-
import org.apache.commons.codec.binary.Base64
32-
import org.apache.commons.codec.binary.StringUtils
3329
import org.apache.hadoop.hive.shims.Utils
3430
import org.apache.thrift.TProcessor
3531
import org.apache.thrift.protocol.TProtocolFactory
3632
import org.apache.thrift.server.TServlet
3733

3834
import org.apache.kyuubi.Logging
3935
import org.apache.kyuubi.config.KyuubiConf
36+
import org.apache.kyuubi.server.http.authentication.AuthenticationFilter
37+
import org.apache.kyuubi.server.http.authentication.AuthenticationHandler.AUTHORIZATION_HEADER
4038
import org.apache.kyuubi.server.http.util.{CookieSigner, HttpAuthUtils, SessionManager}
41-
import org.apache.kyuubi.service.authentication.{AuthenticationProviderFactory, KyuubiAuthenticationFactory}
39+
import org.apache.kyuubi.service.authentication.KyuubiAuthenticationFactory
4240

4341
class ThriftHttpServlet(
4442
processor: TProcessor,
@@ -57,6 +55,7 @@ class ThriftHttpServlet(
5755
private var isCookieSecure = false
5856
private var isHttpOnlyCookie = false
5957
private val X_FORWARDED_FOR_HEADER = "X-Forwarded-For"
58+
private val authenticationFilter = new AuthenticationFilter(conf)
6059

6160
override def init(): Unit = {
6261
isCookieAuthEnabled = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_COOKIE_AUTH_ENABLED)
@@ -72,6 +71,7 @@ class ThriftHttpServlet(
7271
isCookieSecure = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_USE_SSL)
7372
isHttpOnlyCookie = conf.get(KyuubiConf.FRONTEND_THRIFT_HTTP_COOKIE_IS_HTTPONLY)
7473
}
74+
authenticationFilter.initAuthHandlers()
7575
}
7676

7777
@throws[ServletException]
@@ -105,10 +105,10 @@ class ThriftHttpServlet(
105105
// If the cookie based authentication is not enabled or the request does not have a valid
106106
// cookie, use authentication depending on the server setup.
107107
if (clientUserName == null) {
108-
clientUserName = doPasswdAuth(request, authFactory)
108+
clientUserName = authenticate(request, response)
109109
}
110110

111-
assert(clientUserName != null)
111+
require(clientUserName != null, "No valid authorization provided")
112112
debug("Client username: " + clientUserName)
113113

114114
// Set the thread local username to be used for doAs if true
@@ -280,100 +280,10 @@ class ThriftHttpServlet(
280280
newCookie + "; HttpOnly"
281281
}
282282

283-
/**
284-
* Do the LDAP/PAM authentication
285-
*
286-
* @param request
287-
* @param authFactory
288-
* @throws AuthenticationException
289-
*/
290-
@throws[AuthenticationException]
291-
private def doPasswdAuth(
292-
request: HttpServletRequest,
293-
authFactory: KyuubiAuthenticationFactory): String = {
294-
val userName = getUsername(request, authFactory: KyuubiAuthenticationFactory)
295-
debug("Is No SASL Enabled : " + authFactory.isNoSaslEnabled)
296-
// No-op when authType is NOSASL
297-
if (authFactory.isNoSaslEnabled) return userName
298-
try {
299-
debug("Initiating Password Authentication")
300-
val password = getPassword(request, authFactory)
301-
val authMethod = authFactory.getValidPasswordAuthMethod
302-
debug("Password Method: " + authMethod)
303-
val provider = AuthenticationProviderFactory.getAuthenticationProvider(authMethod, conf)
304-
debug("Password Provider obtained")
305-
provider.authenticate(userName, password)
306-
debug("Password Provider authenticated username successfully")
307-
} catch {
308-
case e: Exception =>
309-
throw new AuthenticationException(e.getMessage, e)
310-
}
311-
userName
312-
}
313-
314-
@throws[AuthenticationException]
315-
private def getUsername(
316-
request: HttpServletRequest,
317-
authFactory: KyuubiAuthenticationFactory): String = {
318-
val creds = getAuthHeaderTokens(request, authFactory)
319-
// Username must be present
320-
if (creds(0) == null || creds(0).isEmpty) {
321-
throw new AuthenticationException("Authorization header received " +
322-
"from the client does not contain username.")
323-
}
324-
creds(0)
325-
}
326-
327-
@throws[AuthenticationException]
328-
private def getPassword(
329-
request: HttpServletRequest,
330-
authFactory: KyuubiAuthenticationFactory): String = {
331-
val creds = getAuthHeaderTokens(request, authFactory)
332-
// Password must be present
333-
if (creds(1) == null || creds(1).isEmpty) {
334-
throw new AuthenticationException("Authorization header received " +
335-
"from the client does not contain username.")
336-
}
337-
creds(1)
338-
}
339-
340-
@throws[AuthenticationException]
341-
private def getAuthHeaderTokens(
342-
request: HttpServletRequest,
343-
authFactory: KyuubiAuthenticationFactory): Array[String] = {
344-
val authHeaderBase64 = getAuthHeader(request, authFactory)
345-
val authHeaderString = StringUtils.newStringUtf8(Base64.decodeBase64(authHeaderBase64.getBytes))
346-
authHeaderString.split(":")
347-
}
348-
349-
/**
350-
* Returns the base64 encoded auth header payload
351-
*
352-
* @param request
353-
* @param authFactory
354-
* @return
355-
* @throws AuthenticationException
356-
*/
357-
@throws[AuthenticationException]
358-
private def getAuthHeader(
359-
request: HttpServletRequest,
360-
authFactory: KyuubiAuthenticationFactory): String = {
361-
val authHeader = request.getHeader(HttpAuthUtils.AUTHORIZATION)
362-
// Each http request must have an Authorization header
363-
if (authHeader == null || authHeader.isEmpty) {
364-
throw new AuthenticationException("Authorization header received " +
365-
"from the client is empty.")
366-
}
367-
368-
var authHeaderBase64String: String = null
369-
val beginIndex = (HttpAuthUtils.BASIC + " ").length
370-
authHeaderBase64String = authHeader.substring(beginIndex)
371-
// Authorization header must have a payload
372-
if (authHeaderBase64String == null || authHeaderBase64String.isEmpty) {
373-
throw new AuthenticationException("Authorization header received " +
374-
"from the client does not contain any data.")
375-
}
376-
authHeaderBase64String
283+
private def authenticate(request: HttpServletRequest, response: HttpServletResponse): String = {
284+
val authorization = request.getHeader(AUTHORIZATION_HEADER)
285+
authenticationFilter.getMatchedHandler(authorization).map(
286+
_.authenticate(request, response)).orNull
377287
}
378288

379289
private def getDoAsQueryParam(queryString: String): String = {

kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilter.scala

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class AuthenticationFilter(conf: KyuubiConf) extends Filter with Logging {
5353
}
5454
}
5555

56-
override def init(filterConfig: FilterConfig): Unit = {
56+
private[kyuubi] def initAuthHandlers(): Unit = {
5757
val authTypes = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName)
5858
val spnegoKerberosEnabled = authTypes.contains(KERBEROS)
5959
val basicAuthTypeOpt = {
@@ -75,9 +75,17 @@ class AuthenticationFilter(conf: KyuubiConf) extends Filter with Logging {
7575
val internalHandler = new KyuubiInternalAuthenticationHandler
7676
addAuthHandler(internalHandler)
7777
}
78+
}
79+
80+
override def init(filterConfig: FilterConfig): Unit = {
81+
initAuthHandlers()
7882
super.init(filterConfig)
7983
}
8084

85+
private[kyuubi] def getMatchedHandler(authorization: String): Option[AuthenticationHandler] = {
86+
authSchemeHandlers.values.find(_.matchAuthScheme(authorization))
87+
}
88+
8189
/**
8290
* If the request has a valid authentication token it allows the request to continue to the
8391
* target resource, otherwise it triggers an authentication sequence using the configured
@@ -97,13 +105,7 @@ class AuthenticationFilter(conf: KyuubiConf) extends Filter with Logging {
97105
val httpResponse = response.asInstanceOf[HttpServletResponse]
98106

99107
val authorization = httpRequest.getHeader(AUTHORIZATION_HEADER)
100-
var matchedHandler: AuthenticationHandler = null
101-
102-
for (authHandler <- authSchemeHandlers.values if matchedHandler == null) {
103-
if (authHandler.matchAuthScheme(authorization)) {
104-
matchedHandler = authHandler
105-
}
106-
}
108+
val matchedHandler = getMatchedHandler(authorization).orNull
107109

108110
if (matchedHandler == null) {
109111
debug(s"No auth scheme matched for url: ${httpRequest.getRequestURL}")

kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/AuthenticationHandler.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ trait AuthenticationHandler {
9797
throw new AuthenticationException("Authorization header received from the client is empty.")
9898
}
9999

100-
val authorization = authHeader.substring(authScheme.toString.length).trim
100+
var authorization = authHeader.substring(authScheme.toString.length).trim
101+
// For thrift http spnego authorization, its format is 'NEGOTIATE : $token', see HIVE-26353
102+
if (authorization.startsWith(":")) {
103+
authorization = authorization.stripPrefix(":").trim
104+
}
101105
// Authorization header must have a payload
102106
if (authorization == null || authorization.isEmpty()) {
103107
throw new AuthenticationException(

kyuubi-server/src/test/scala/org/apache/kyuubi/operation/KyuubiOperationKerberosAndPlainAuthSuite.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class KyuubiOperationKerberosAndPlainAuthSuite extends WithKyuubiServer with Ker
3232
private val customPasswd: String = "password"
3333

3434
override protected def jdbcUrl: String = getJdbcUrl
35-
private def kerberosJdbcUrl: String = jdbcUrl + s"principal=${testPrincipal}"
35+
protected def kerberosJdbcUrl: String = jdbcUrl.stripSuffix(";") + s";principal=${testPrincipal}"
3636
private val currentUser = UserGroupInformation.getCurrentUser
3737

3838
override def beforeAll(): Unit = {

kyuubi-server/src/test/scala/org/apache/kyuubi/operation/thrift/http/KyuubiOperationThriftHttpKerberosAndPlainAuthSuite.scala

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,43 @@
1717

1818
package org.apache.kyuubi.operation.thrift.http
1919

20-
import org.scalactic.source.Position
21-
import org.scalatest.Tag
20+
import org.apache.hadoop.conf.Configuration
21+
import org.apache.hadoop.security.UserGroupInformation
2222

2323
import org.apache.kyuubi.config.KyuubiConf
2424
import org.apache.kyuubi.config.KyuubiConf.FrontendProtocols
2525
import org.apache.kyuubi.operation.KyuubiOperationKerberosAndPlainAuthSuite
26+
import org.apache.kyuubi.service.authentication.UserDefineAuthenticationProviderImpl
2627

2728
class KyuubiOperationThriftHttpKerberosAndPlainAuthSuite
2829
extends KyuubiOperationKerberosAndPlainAuthSuite {
2930
override protected val frontendProtocols: Seq[KyuubiConf.FrontendProtocols.Value] =
3031
FrontendProtocols.THRIFT_HTTP :: Nil
3132

33+
override protected def kerberosJdbcUrl: String =
34+
jdbcUrl.stripSuffix(";") + s";principal=${testSpnegoPrincipal}"
35+
36+
override protected lazy val conf: KyuubiConf = {
37+
val config = new Configuration()
38+
val authType = "hadoop.security.authentication"
39+
config.set(authType, "KERBEROS")
40+
System.setProperty("java.security.krb5.conf", krb5ConfPath)
41+
UserGroupInformation.setConfiguration(config)
42+
assert(UserGroupInformation.isSecurityEnabled)
43+
44+
KyuubiConf().set(KyuubiConf.AUTHENTICATION_METHOD, Seq("KERBEROS", "LDAP", "CUSTOM"))
45+
.set(KyuubiConf.SERVER_KEYTAB, testKeytab)
46+
.set(KyuubiConf.SERVER_PRINCIPAL, testPrincipal)
47+
.set(KyuubiConf.AUTHENTICATION_LDAP_URL, ldapUrl)
48+
.set(KyuubiConf.AUTHENTICATION_LDAP_BASEDN, ldapBaseDn)
49+
.set(
50+
KyuubiConf.AUTHENTICATION_CUSTOM_CLASS,
51+
classOf[UserDefineAuthenticationProviderImpl].getCanonicalName)
52+
.set(KyuubiConf.SERVER_SPNEGO_KEYTAB, testKeytab)
53+
.set(KyuubiConf.SERVER_SPNEGO_PRINCIPAL, testSpnegoPrincipal)
54+
}
55+
3256
override protected def getJdbcUrl: String =
3357
s"jdbc:hive2://${server.frontendServices.head.connectionUrl}/default;transportMode=http;" +
3458
s"httpPath=cliservice"
35-
36-
override protected def test(testName: String, testTags: Tag*)(testFun: => Any)(implicit
37-
pos: Position): Unit = {
38-
if (!testName.equals("test with KERBEROS authentication")) {
39-
super.test(testName, testTags: _*)(testFun)(pos)
40-
}
41-
}
4259
}

0 commit comments

Comments
 (0)