Skip to content

Commit d0f75e8

Browse files
committed
[KYUUBI #3222][FOLLOWUP] Introdude JdbcUtils to simplify code
### _Why are the changes needed?_ This is the followup of #3235, the main change is introdude `JdbcUtils` to simplify code, and allow empty password for Jdbc auth. Jdbc connection pool has been removed because `JdbcAuthenticationProviderImpl` will be created on each connection, we can improve to use singleton in the future ### _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 - [x] [Run test](https://kyuubi.apache.org/docs/latest/develop_tools/testing.html#running-tests) locally before make a pull request Closes #3278 from pan3793/jdbc-followup. Closes #3222 2863cae [Cheng Pan] Update kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/JdbcAuthenticationProviderImplSuite.scala 51a9c45 [Cheng Pan] Update kyuubi-common/src/test/scala/org/apache/kyuubi/service/authentication/JdbcAuthenticationProviderImplSuite.scala eee3c55 [Cheng Pan] Update kyuubi-common/src/test/scala/org/apache/kyuubi/util/JdbcUtilsSuite.scala d02bb99 [Cheng Pan] nit e001b5b [Cheng Pan] nit 8cf5cd6 [Cheng Pan] nit 032f2df [Cheng Pan] nit 8a42f18 [Cheng Pan] nit c7893fd [Cheng Pan] JdbcUtilsSuite f97f2d9 [Cheng Pan] remove pool a8812d0 [Cheng Pan] move render result set to test 83d7d4c [Cheng Pan] fix ut db787a4 [Cheng Pan] nit 864f9dd [Cheng Pan] nit b60decf [Cheng Pan] nit 8c66e0b [Cheng Pan] nit 2063c43 [Cheng Pan] [KYUUBI #3222][FOLLOWUP] Introdude JdbcUtils to simplify code Authored-by: Cheng Pan <[email protected]> Signed-off-by: Cheng Pan <[email protected]>
1 parent 327336f commit d0f75e8

File tree

6 files changed

+300
-203
lines changed

6 files changed

+300
-203
lines changed

kyuubi-common/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@
148148
<artifactId>derby</artifactId>
149149
<scope>test</scope>
150150
</dependency>
151+
152+
<dependency>
153+
<groupId>com.jakewharton.fliptables</groupId>
154+
<artifactId>fliptables</artifactId>
155+
<scope>test</scope>
156+
</dependency>
151157
</dependencies>
152158

153159
<build>

kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/JdbcAuthenticationProviderImpl.scala

Lines changed: 60 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,44 @@
1717

1818
package org.apache.kyuubi.service.authentication
1919

20-
import java.sql.{Connection, PreparedStatement, Statement}
2120
import java.util.Properties
2221
import javax.security.sasl.AuthenticationException
22+
import javax.sql.DataSource
2323

24-
import com.zaxxer.hikari.{HikariConfig, HikariDataSource}
24+
import com.zaxxer.hikari.util.DriverDataSource
2525
import org.apache.commons.lang3.StringUtils
2626

2727
import org.apache.kyuubi.Logging
2828
import org.apache.kyuubi.config.KyuubiConf
2929
import org.apache.kyuubi.config.KyuubiConf._
30+
import org.apache.kyuubi.util.JdbcUtils
3031

3132
class JdbcAuthenticationProviderImpl(conf: KyuubiConf) extends PasswdAuthenticationProvider
3233
with Logging {
3334

34-
private val driverClass = conf.get(AUTHENTICATION_JDBC_DRIVER)
35-
private val jdbcUrl = conf.get(AUTHENTICATION_JDBC_URL)
36-
private val jdbcUsername = conf.get(AUTHENTICATION_JDBC_USERNAME)
37-
private val jdbcUserPassword = conf.get(AUTHENTICATION_JDBC_PASSWORD)
38-
private val authQuerySql = conf.get(AUTHENTICATION_JDBC_QUERY)
39-
4035
private val SQL_PLACEHOLDER_REGEX = """\$\{.+?}""".r
4136
private val USERNAME_SQL_PLACEHOLDER = "${username}"
4237
private val PASSWORD_SQL_PLACEHOLDER = "${password}"
4338

39+
private val driverClass = conf.get(AUTHENTICATION_JDBC_DRIVER)
40+
private val jdbcUrl = conf.get(AUTHENTICATION_JDBC_URL)
41+
private val username = conf.get(AUTHENTICATION_JDBC_USERNAME)
42+
private val password = conf.get(AUTHENTICATION_JDBC_PASSWORD)
43+
private val authQuery = conf.get(AUTHENTICATION_JDBC_QUERY)
44+
45+
private val redactedPasswd = password match {
46+
case Some(s) if !StringUtils.isBlank(s) => s"${"*" * s.length}(length: ${s.length})"
47+
case None => "(empty)"
48+
}
49+
4450
checkJdbcConfigs()
4551

46-
private[kyuubi] val hikariDataSource = getHikariDataSource
52+
implicit private[kyuubi] val ds: DataSource = new DriverDataSource(
53+
jdbcUrl.orNull,
54+
driverClass.orNull,
55+
new Properties,
56+
username.orNull,
57+
password.orNull)
4758

4859
/**
4960
* The authenticate method is called by the Kyuubi Server authentication layer
@@ -62,37 +73,27 @@ class JdbcAuthenticationProviderImpl(conf: KyuubiConf) extends PasswdAuthenticat
6273
s" or contains blank space")
6374
}
6475

65-
if (StringUtils.isBlank(password)) {
66-
throw new AuthenticationException(s"Error validating, password is null" +
67-
s" or contains blank space")
68-
}
69-
70-
var connection: Connection = null
71-
var queryStatement: PreparedStatement = null
72-
7376
try {
74-
connection = hikariDataSource.getConnection
75-
76-
queryStatement = getAndPrepareQueryStatement(connection, user, password)
77-
78-
val resultSet = queryStatement.executeQuery()
79-
80-
if (resultSet == null || !resultSet.next()) {
81-
// auth failed
82-
throw new AuthenticationException(s"Password does not match or no such user. user:" +
83-
s" $user , password length: ${password.length}")
77+
debug(s"prepared auth query: $preparedQuery")
78+
JdbcUtils.executeQuery(preparedQuery) { stmt =>
79+
stmt.setMaxRows(1) // minimum result size required for authentication
80+
queryPlaceholders.zipWithIndex.foreach {
81+
case (USERNAME_SQL_PLACEHOLDER, i) => stmt.setString(i + 1, user)
82+
case (PASSWORD_SQL_PLACEHOLDER, i) => stmt.setString(i + 1, password)
83+
case (p, _) => throw new IllegalArgumentException(
84+
s"Unrecognized placeholder in Query SQL: $p")
85+
}
86+
} { resultSet =>
87+
if (resultSet == null || !resultSet.next()) {
88+
throw new AuthenticationException("Password does not match or no such user. " +
89+
s"user: $user, password: $redactedPasswd")
90+
}
8491
}
85-
86-
// auth passed
87-
8892
} catch {
89-
case e: AuthenticationException =>
90-
throw e
91-
case e: Exception =>
92-
error("Cannot get user info", e);
93-
throw e
94-
} finally {
95-
closeDbConnection(connection, queryStatement)
93+
case rethrow: AuthenticationException =>
94+
throw rethrow
95+
case rethrow: Exception =>
96+
throw new AuthenticationException("Cannot get user info", rethrow)
9697
}
9798
}
9899

@@ -101,104 +102,34 @@ class JdbcAuthenticationProviderImpl(conf: KyuubiConf) extends PasswdAuthenticat
101102

102103
debug(configLog("Driver Class", driverClass.orNull))
103104
debug(configLog("JDBC URL", jdbcUrl.orNull))
104-
debug(configLog("Database username", jdbcUsername.orNull))
105-
debug(configLog("Database password length", jdbcUserPassword.getOrElse("").length.toString))
106-
debug(configLog("Query SQL", authQuerySql.orNull))
105+
debug(configLog("Database username", username.orNull))
106+
debug(configLog("Database password", redactedPasswd))
107+
debug(configLog("Query SQL", authQuery.orNull))
107108

108109
// Check if JDBC parameters valid
109-
if (driverClass.isEmpty) {
110-
throw new IllegalArgumentException("JDBC driver class is not configured.")
111-
}
112-
113-
if (jdbcUrl.isEmpty) {
114-
throw new IllegalArgumentException("JDBC url is not configured")
115-
}
116-
117-
if (jdbcUsername.isEmpty || jdbcUserPassword.isEmpty) {
118-
throw new IllegalArgumentException("JDBC username or password is not configured")
110+
require(driverClass.nonEmpty, "JDBC driver class is not configured.")
111+
require(jdbcUrl.nonEmpty, "JDBC url is not configured.")
112+
require(username.nonEmpty, "JDBC username is not configured")
113+
// allow empty password
114+
require(authQuery.nonEmpty, "Query SQL is not configured")
115+
116+
val query = authQuery.get.trim.toLowerCase
117+
// allow simple select query sql only, complex query like CTE is not allowed
118+
require(query.startsWith("select"), "Query SQL must start with 'SELECT'")
119+
if (!query.contains("where")) {
120+
warn("Query SQL does not contains 'WHERE' keyword")
119121
}
120-
121-
// Check Query SQL
122-
if (authQuerySql.isEmpty) {
123-
throw new IllegalArgumentException("Query SQL is not configured")
124-
}
125-
val querySqlInLowerCase = authQuerySql.get.trim.toLowerCase
126-
if (!querySqlInLowerCase.startsWith("select")) { // allow select query sql only
127-
throw new IllegalArgumentException("Query SQL must start with \"SELECT\"");
128-
}
129-
if (!querySqlInLowerCase.contains("where")) {
130-
warn("Query SQL does not contains \"WHERE\" keyword");
122+
if (!query.contains(USERNAME_SQL_PLACEHOLDER)) {
123+
warn(s"Query SQL does not contains '$USERNAME_SQL_PLACEHOLDER' placeholder")
131124
}
132-
if (!querySqlInLowerCase.contains("${username}")) {
133-
warn("Query SQL does not contains \"${username}\" placeholder");
125+
if (!query.contains(PASSWORD_SQL_PLACEHOLDER)) {
126+
warn(s"Query SQL does not contains '$PASSWORD_SQL_PLACEHOLDER' placeholder")
134127
}
135128
}
136129

137-
private def getPlaceholderList(sql: String): List[String] = {
138-
SQL_PLACEHOLDER_REGEX.findAllMatchIn(sql)
139-
.map(m => m.matched)
140-
.toList
141-
}
142-
143-
private def getAndPrepareQueryStatement(
144-
connection: Connection,
145-
user: String,
146-
password: String): PreparedStatement = {
130+
private def preparedQuery: String =
131+
SQL_PLACEHOLDER_REGEX.replaceAllIn(authQuery.get, "?")
147132

148-
val preparedSql: String = {
149-
SQL_PLACEHOLDER_REGEX.replaceAllIn(authQuerySql.get, "?")
150-
}
151-
debug(s"prepared auth query sql: $preparedSql")
152-
153-
val stmt = connection.prepareStatement(preparedSql)
154-
stmt.setMaxRows(1) // minimum result size required for authentication
155-
156-
// Extract placeholder list and fill parameters to placeholders
157-
val placeholderList: List[String] = getPlaceholderList(authQuerySql.get)
158-
for (i <- placeholderList.indices) {
159-
val param = placeholderList(i) match {
160-
case USERNAME_SQL_PLACEHOLDER => user
161-
case PASSWORD_SQL_PLACEHOLDER => password
162-
case otherPlaceholder =>
163-
throw new IllegalArgumentException(
164-
s"Unrecognized Placeholder In Query SQL: $otherPlaceholder")
165-
}
166-
167-
stmt.setString(i + 1, param)
168-
}
169-
170-
stmt
171-
}
172-
173-
private def closeDbConnection(connection: Connection, statement: Statement): Unit = {
174-
if (statement != null && !statement.isClosed) {
175-
try {
176-
statement.close()
177-
} catch {
178-
case e: Exception =>
179-
error("Cannot close PreparedStatement to auth database ", e)
180-
}
181-
}
182-
183-
if (connection != null && !connection.isClosed) {
184-
try {
185-
connection.close()
186-
} catch {
187-
case e: Exception =>
188-
error("Cannot close connection to auth database ", e)
189-
}
190-
}
191-
}
192-
193-
private def getHikariDataSource: HikariDataSource = {
194-
val datasourceProperties = new Properties()
195-
val hikariConfig = new HikariConfig(datasourceProperties)
196-
hikariConfig.setDriverClassName(driverClass.orNull)
197-
hikariConfig.setJdbcUrl(jdbcUrl.orNull)
198-
hikariConfig.setUsername(jdbcUsername.orNull)
199-
hikariConfig.setPassword(jdbcUserPassword.orNull)
200-
hikariConfig.setPoolName("jdbc-auth-pool")
201-
202-
new HikariDataSource(hikariConfig)
203-
}
133+
private def queryPlaceholders: Iterator[String] =
134+
SQL_PLACEHOLDER_REGEX.findAllMatchIn(authQuery.get).map(_.matched)
204135
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.util
19+
20+
import java.sql.{Connection, PreparedStatement, ResultSet}
21+
import javax.sql.DataSource
22+
23+
import scala.util.control.NonFatal
24+
25+
import org.apache.kyuubi.Logging
26+
27+
object JdbcUtils extends Logging {
28+
29+
def close(c: AutoCloseable): Unit = {
30+
if (c != null) {
31+
try {
32+
c.close()
33+
} catch {
34+
case NonFatal(t) => warn(s"Error on closing", t)
35+
}
36+
}
37+
}
38+
39+
def withCloseable[R, C <: AutoCloseable](c: C)(block: C => R): R = {
40+
try {
41+
block(c)
42+
} finally {
43+
close(c)
44+
}
45+
}
46+
47+
def withConnection[R](block: Connection => R)(implicit ds: DataSource): R = {
48+
withCloseable(ds.getConnection)(block)
49+
}
50+
51+
def execute(
52+
sqlTemplate: String)(
53+
setParameters: PreparedStatement => Unit = _ => {})(
54+
implicit ds: DataSource): Boolean = withConnection { conn =>
55+
withCloseable(conn.prepareStatement(sqlTemplate)) { pStmt =>
56+
setParameters(pStmt)
57+
pStmt.execute()
58+
}
59+
}
60+
61+
def executeUpdate(
62+
sqlTemplate: String)(
63+
setParameters: PreparedStatement => Unit = _ => {})(
64+
implicit ds: DataSource): Int = withConnection { conn =>
65+
withCloseable(conn.prepareStatement(sqlTemplate)) { pStmt =>
66+
setParameters(pStmt)
67+
pStmt.executeUpdate()
68+
}
69+
}
70+
71+
def executeQuery[R](
72+
sqlTemplate: String)(
73+
setParameters: PreparedStatement => Unit = _ => {})(
74+
processResultSet: ResultSet => R)(
75+
implicit ds: DataSource): R = withConnection { conn =>
76+
withCloseable(conn.prepareStatement(sqlTemplate)) { pStmt =>
77+
setParameters(pStmt)
78+
withCloseable(pStmt.executeQuery()) { rs =>
79+
processResultSet(rs)
80+
}
81+
}
82+
}
83+
84+
def executeQueryWithRowMapper[R](
85+
sqlTemplate: String)(
86+
setParameters: PreparedStatement => Unit = _ => {})(
87+
rowMapper: ResultSet => R)(
88+
implicit ds: DataSource): Seq[R] = withConnection { conn =>
89+
withCloseable(conn.prepareStatement(sqlTemplate)) { pStmt =>
90+
setParameters(pStmt)
91+
withCloseable(pStmt.executeQuery()) { rs =>
92+
val builder = Seq.newBuilder[R]
93+
while (rs.next()) builder += rowMapper(rs)
94+
builder.result
95+
}
96+
}
97+
}
98+
}

kyuubi-common/src/test/scala/org/apache/kyuubi/TestUtils.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ package org.apache.kyuubi
1919

2020
import java.nio.charset.StandardCharsets
2121
import java.nio.file.{Files, Path, StandardOpenOption}
22+
import java.sql.ResultSet
2223

2324
import scala.collection.mutable.ArrayBuffer
2425

26+
import com.jakewharton.fliptables.FlipTable
2527
import org.scalatest.Assertions.convertToEqualizer
2628

2729
object TestUtils {
@@ -59,4 +61,18 @@ object TestUtils {
5961
newOutput.zip(expected).foreach { case (out, in) => assert(out === in, hint) }
6062
}
6163
}
64+
65+
def displayResultSet(resultSet: ResultSet): Unit = {
66+
if (resultSet == null) throw new NullPointerException("resultSet == null")
67+
val resultSetMetaData = resultSet.getMetaData
68+
val columnCount: Int = resultSetMetaData.getColumnCount
69+
val headers = (1 to columnCount).map(resultSetMetaData.getColumnName).toArray
70+
val data = ArrayBuffer.newBuilder[Array[String]]
71+
while (resultSet.next) {
72+
data += (1 to columnCount).map(resultSet.getString).toArray
73+
}
74+
// scalastyle:off println
75+
println(FlipTable.of(headers, data.result().toArray))
76+
// scalastyle:on println
77+
}
6278
}

0 commit comments

Comments
 (0)