Skip to content

Commit 9bd9105

Browse files
committed
[KYUUBI #2300] Add http UGIAssuming handler wrapper for kerberos enabled restful frontend service
### _Why are the changes needed?_ Wrap the http handler with PrivilegedAction for kerberos enabled use case. ### _How was this patch tested?_ - [ ] Add some test cases that check the changes thoroughly including negative and positive cases if possible - [ ] Add screenshots for manual tests if appropriate - [ ] [Run test](https://kyuubi.apache.org/docs/latest/develop_tools/testing.html#running-tests) locally before make a pull request Closes #2300 from turboFei/ugi_wrapper. Closes #2300 da07406 [Fei Wang] add ut a56fe67 [Fei Wang] verify proxy user for rest open session 9e0f228 [Fei Wang] remove blank 8070d13 [Fei Wang] revert KyuubiAuthenticationFactory change d40fa13 [Fei Wang] Add ugi assuming http handler wrapper for kerberos enabeld restful front Authored-by: Fei Wang <fwang12@ebay.com> Signed-off-by: Fei Wang <fwang12@ebay.com>
1 parent 9362761 commit 9bd9105

File tree

5 files changed

+139
-32
lines changed

5 files changed

+139
-32
lines changed

kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,18 @@ import java.util.EnumSet
2121
import java.util.concurrent.atomic.AtomicBoolean
2222
import javax.servlet.DispatcherType
2323

24+
import org.apache.hadoop.conf.Configuration
2425
import org.eclipse.jetty.servlet.FilterHolder
2526

2627
import org.apache.kyuubi.{KyuubiException, Utils}
2728
import org.apache.kyuubi.config.KyuubiConf
2829
import org.apache.kyuubi.config.KyuubiConf.{FRONTEND_REST_BIND_HOST, FRONTEND_REST_BIND_PORT}
29-
import org.apache.kyuubi.server.api.v1.ApiRootResource
30-
import org.apache.kyuubi.server.http.authentication.AuthenticationFilter
30+
import org.apache.kyuubi.server.api.v1.{ApiRootResource, SessionOpenRequest}
31+
import org.apache.kyuubi.server.http.authentication.{AuthenticationFilter, KyuubiHttpAuthenticationFactory}
3132
import org.apache.kyuubi.server.ui.JettyServer
32-
import org.apache.kyuubi.service.{AbstractFrontendService, Serverable, Service}
33+
import org.apache.kyuubi.service.{AbstractFrontendService, Serverable, Service, ServiceUtils}
34+
import org.apache.kyuubi.service.authentication.KyuubiAuthenticationFactory
35+
import org.apache.kyuubi.util.KyuubiHadoopUtils
3336

3437
/**
3538
* A frontend service based on RESTful api via HTTP protocol.
@@ -42,6 +45,8 @@ class KyuubiRestFrontendService(override val serverable: Serverable)
4245

4346
private val isStarted = new AtomicBoolean(false)
4447

48+
private lazy val hadoopConf: Configuration = KyuubiHadoopUtils.newHadoopConf(conf)
49+
4550
override def initialize(conf: KyuubiConf): Unit = synchronized {
4651
val host = conf.get(FRONTEND_REST_BIND_HOST)
4752
.getOrElse(Utils.findLocalInetAddress.getHostAddress)
@@ -58,7 +63,8 @@ class KyuubiRestFrontendService(override val serverable: Serverable)
5863
val contextHandler = ApiRootResource.getServletHandler(this)
5964
val holder = new FilterHolder(new AuthenticationFilter(conf))
6065
contextHandler.addFilter(holder, "/*", EnumSet.allOf(classOf[DispatcherType]))
61-
server.addHandler(contextHandler)
66+
val authenticationFactory = new KyuubiHttpAuthenticationFactory(conf)
67+
server.addHandler(authenticationFactory.httpHandlerWrapperFactory.wrapHandler(contextHandler))
6268

6369
server.addStaticHandler("org/apache/kyuubi/ui/static", "/static")
6470
server.addRedirectHandler("/", "/static")
@@ -87,5 +93,25 @@ class KyuubiRestFrontendService(override val serverable: Serverable)
8793
super.stop()
8894
}
8995

96+
def getUserName(req: SessionOpenRequest): String = {
97+
val realUser: String =
98+
ServiceUtils.getShortName(Option(AuthenticationFilter.getUserName).getOrElse(req.user))
99+
if (req.configs == null) {
100+
realUser
101+
} else {
102+
getProxyUser(req.configs, Option(AuthenticationFilter.getUserIpAddress).orNull, realUser)
103+
}
104+
}
105+
106+
private def getProxyUser(
107+
sessionConf: Map[String, String],
108+
ipAddress: String,
109+
realUser: String): String = {
110+
sessionConf.get(KyuubiAuthenticationFactory.HS2_PROXY_USER).map { proxyUser =>
111+
KyuubiAuthenticationFactory.verifyProxyAccess(realUser, proxyUser, ipAddress, hadoopConf)
112+
proxyUser
113+
}.getOrElse(realUser)
114+
}
115+
90116
override val discoveryService: Option[Service] = None
91117
}

kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import org.apache.kyuubi.Logging
3131
import org.apache.kyuubi.events.KyuubiEvent
3232
import org.apache.kyuubi.operation.OperationHandle
3333
import org.apache.kyuubi.server.api.ApiRequestContext
34+
import org.apache.kyuubi.server.http.authentication.AuthenticationFilter
3435
import org.apache.kyuubi.session.SessionHandle
3536
import org.apache.kyuubi.session.SessionHandle.parseSessionHandle
3637

@@ -130,12 +131,14 @@ private[v1] class SessionsResource extends ApiRequestContext with Logging {
130131
@POST
131132
@Consumes(Array(MediaType.APPLICATION_JSON))
132133
def openSession(request: SessionOpenRequest): SessionHandle = {
134+
val userName = fe.getUserName(request)
135+
val ipAddress = AuthenticationFilter.getUserIpAddress
133136
fe.be.openSession(
134137
TProtocolVersion.findByValue(request.protocolVersion),
135-
request.user,
138+
userName,
136139
request.password,
137-
request.ipAddr,
138-
request.configs)
140+
ipAddress,
141+
Option(request.configs).getOrElse(Map.empty[String, String]))
139142
}
140143

141144
@ApiResponse(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.kyuubi.server.http.authentication
19+
20+
import java.security.PrivilegedAction
21+
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
22+
23+
import org.apache.hadoop.security.UserGroupInformation
24+
import org.eclipse.jetty.server.{Handler, Request}
25+
import org.eclipse.jetty.server.handler.HandlerWrapper
26+
27+
import org.apache.kyuubi.config.KyuubiConf
28+
import org.apache.kyuubi.config.KyuubiConf.{AUTHENTICATION_METHOD, ENGINE_SECURITY_ENABLED}
29+
import org.apache.kyuubi.service.authentication.{AuthTypes, EngineSecurityAccessor}
30+
import org.apache.kyuubi.service.authentication.AuthTypes.KERBEROS
31+
32+
class KyuubiHttpAuthenticationFactory(conf: KyuubiConf) {
33+
private val authTypes = conf.get(AUTHENTICATION_METHOD).map(AuthTypes.withName)
34+
private val kerberosEnabled = authTypes.contains(KERBEROS)
35+
private val ugi = UserGroupInformation.getCurrentUser
36+
37+
if (conf.get(ENGINE_SECURITY_ENABLED)) {
38+
EngineSecurityAccessor.initialize(conf, true)
39+
}
40+
41+
private[kyuubi] val httpHandlerWrapperFactory =
42+
new HttpHandlerWrapperFactory(ugi, kerberosEnabled)
43+
44+
class HttpHandlerWrapperFactory(ugi: UserGroupInformation, kerberosEnabled: Boolean) {
45+
def wrapHandler(handler: Handler): HandlerWrapper = {
46+
new HandlerWrapper {
47+
_handler = handler
48+
49+
override def handle(
50+
target: String,
51+
baseRequest: Request,
52+
request: HttpServletRequest,
53+
response: HttpServletResponse): Unit = {
54+
try {
55+
if (kerberosEnabled) {
56+
ugi.doAs(new PrivilegedAction[Unit] {
57+
override def run(): Unit = {
58+
handler.handle(target, baseRequest, request, response)
59+
}
60+
})
61+
} else {
62+
handler.handle(target, baseRequest, request, response)
63+
}
64+
} finally {
65+
AuthenticationFilter.HTTP_CLIENT_USER_NAME.remove()
66+
AuthenticationFilter.HTTP_CLIENT_IP_ADDRESS.remove()
67+
}
68+
}
69+
70+
override def doStart(): Unit = {
71+
super.doStart()
72+
handler.start()
73+
}
74+
}
75+
}
76+
}
77+
}

kyuubi-server/src/main/scala/org/apache/kyuubi/server/ui/JettyServer.scala

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,12 @@
1717

1818
package org.apache.kyuubi.server.ui
1919

20-
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
21-
22-
import org.eclipse.jetty.server.{HttpConfiguration, HttpConnectionFactory, Request, Server, ServerConnector}
23-
import org.eclipse.jetty.server.handler.{ContextHandlerCollection, ErrorHandler, HandlerWrapper}
24-
import org.eclipse.jetty.servlet.ServletContextHandler
20+
import org.eclipse.jetty.server.{Handler, HttpConfiguration, HttpConnectionFactory, Server, ServerConnector}
21+
import org.eclipse.jetty.server.handler.{ContextHandlerCollection, ErrorHandler}
2522
import org.eclipse.jetty.util.component.LifeCycle
2623
import org.eclipse.jetty.util.thread.{QueuedThreadPool, ScheduledExecutorScheduler}
2724

2825
import org.apache.kyuubi.Utils.isWindows
29-
import org.apache.kyuubi.server.http.authentication.AuthenticationFilter
3026

3127
private[kyuubi] case class JettyServer(
3228
server: Server,
@@ -55,25 +51,9 @@ private[kyuubi] case class JettyServer(
5551
}
5652
def getServerUri: String = connector.getHost + ":" + connector.getLocalPort
5753

58-
def addHandler(handler: ServletContextHandler): Unit = synchronized {
59-
val handlerWrapper = new HandlerWrapper {
60-
override def handle(
61-
target: String,
62-
baseRequest: Request,
63-
request: HttpServletRequest,
64-
response: HttpServletResponse): Unit = {
65-
try {
66-
super.handle(target, baseRequest, request, response)
67-
} finally {
68-
AuthenticationFilter.HTTP_CLIENT_USER_NAME.remove()
69-
AuthenticationFilter.HTTP_CLIENT_IP_ADDRESS.remove()
70-
}
71-
}
72-
}
73-
handlerWrapper.setHandler(handler)
74-
rootHandler.addHandler(handlerWrapper)
54+
def addHandler(handler: Handler): Unit = synchronized {
55+
rootHandler.addHandler(handler)
7556
if (!handler.isStarted) handler.start()
76-
if (!handlerWrapper.isStarted) handlerWrapper.start()
7757
}
7858

7959
def addStaticHandler(

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ package org.apache.kyuubi.operation
1919

2020
import java.util.Base64
2121
import javax.servlet.http.HttpServletResponse
22+
import javax.ws.rs.client.Entity
23+
import javax.ws.rs.core.MediaType
2224

2325
import org.apache.hadoop.conf.Configuration
2426
import org.apache.hadoop.security.UserGroupInformation
27+
import org.apache.hive.service.rpc.thrift.TProtocolVersion
2528

2629
import org.apache.kyuubi.{KerberizedTestHelper, RestFrontendTestHelper}
2730
import org.apache.kyuubi.config.KyuubiConf
28-
import org.apache.kyuubi.server.api.v1.SessionOpenCount
31+
import org.apache.kyuubi.server.api.v1.{SessionOpenCount, SessionOpenRequest}
2932
import org.apache.kyuubi.server.http.authentication.AuthenticationHandler.AUTHORIZATION_HEADER
3033
import org.apache.kyuubi.service.authentication.{UserDefineAuthenticationProviderImpl, WithLdapServer}
3134

@@ -145,4 +148,22 @@ class KyuubiRestAuthenticationSuite extends RestFrontendTestHelper with Kerberiz
145148
val response = webTarget.path("swagger").request().get()
146149
assert(HttpServletResponse.SC_OK == response.getStatus)
147150
}
151+
152+
test("test with ugi wrapped open session") {
153+
UserGroupInformation.loginUserFromKeytab(testPrincipal, testKeytab)
154+
val token = generateToken(hostName)
155+
val sessionOpenRequest = SessionOpenRequest(
156+
TProtocolVersion.HIVE_CLI_SERVICE_PROTOCOL_V11.getValue,
157+
"kyuubi",
158+
"pass",
159+
"localhost",
160+
Map.empty[String, String])
161+
162+
val response = webTarget.path("api/v1/sessions")
163+
.request()
164+
.header(AUTHORIZATION_HEADER, s"NEGOTIATE $token")
165+
.post(Entity.entity(sessionOpenRequest, MediaType.APPLICATION_JSON_TYPE))
166+
167+
assert(HttpServletResponse.SC_OK == response.getStatus)
168+
}
148169
}

0 commit comments

Comments
 (0)