1717
1818package org .apache .kyuubi .service .authentication
1919
20- import java .sql .{Connection , PreparedStatement , Statement }
2120import java .util .Properties
2221import javax .security .sasl .AuthenticationException
22+ import javax .sql .DataSource
2323
2424import com .zaxxer .hikari .{HikariConfig , HikariDataSource }
2525import org .apache .commons .lang3 .StringUtils
2626
2727import org .apache .kyuubi .Logging
2828import org .apache .kyuubi .config .KyuubiConf
2929import org .apache .kyuubi .config .KyuubiConf ._
30+ import org .apache .kyuubi .util .JdbcUtils
3031
3132class 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 (value) => s " ${" *" * value.length}(length: ${value.length}) "
47+ case None => " (empty)"
48+ }
49+
4450 checkJdbcConfigs()
4551
46- private [kyuubi] val hikariDataSource = getHikariDataSource
52+ implicit private [kyuubi] val ds : DataSource = {
53+ val datasourceProperties = new Properties ()
54+ val hikariConfig = new HikariConfig (datasourceProperties)
55+ hikariConfig.setDriverClassName(driverClass.orNull)
56+ hikariConfig.setJdbcUrl(jdbcUrl.orNull)
57+ hikariConfig.setUsername(username.orNull)
58+ hikariConfig.setPassword(password.orNull)
59+ hikariConfig.setPoolName(" jdbc-auth-pool" )
60+ new HikariDataSource (hikariConfig)
61+ }
4762
4863 /**
4964 * The authenticate method is called by the Kyuubi Server authentication layer
@@ -62,37 +77,27 @@ class JdbcAuthenticationProviderImpl(conf: KyuubiConf) extends PasswdAuthenticat
6277 s " or contains blank space " )
6378 }
6479
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-
7380 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}" )
81+ debug(s " prepared auth query: $preparedQuery" )
82+ JdbcUtils .executeQuery(preparedQuery) { stmt =>
83+ stmt.setMaxRows(1 ) // minimum result size required for authentication
84+ queryPlaceholders.zipWithIndex.foreach {
85+ case (USERNAME_SQL_PLACEHOLDER , i) => stmt.setString(i + 1 , user)
86+ case (PASSWORD_SQL_PLACEHOLDER , i) => stmt.setString(i + 1 , password)
87+ case (p, _) => throw new IllegalArgumentException (
88+ s " Unrecognized placeholder in Query SQL: $p" )
89+ }
90+ } { resultSet =>
91+ if (resultSet == null || ! resultSet.next()) {
92+ throw new AuthenticationException (" Password does not match or no such user. " +
93+ s " user: $user, password: $redactedPasswd" )
94+ }
8495 }
85-
86- // auth passed
87-
8896 } 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)
97+ case rethrow : AuthenticationException =>
98+ throw rethrow
99+ case rethrow : Exception =>
100+ throw new AuthenticationException (" Cannot get user info" , rethrow)
96101 }
97102 }
98103
@@ -101,104 +106,31 @@ class JdbcAuthenticationProviderImpl(conf: KyuubiConf) extends PasswdAuthenticat
101106
102107 debug(configLog(" Driver Class" , driverClass.orNull))
103108 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))
109+ debug(configLog(" Database username" , username .orNull))
110+ debug(configLog(" Database password" , redactedPasswd ))
111+ debug(configLog(" Query SQL" , authQuery .orNull))
107112
108113 // 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" )
114+ require(driverClass.nonEmpty, " JDBC driver class is not configured." )
115+ require(jdbcUrl.nonEmpty, " JDBC url is not configured." )
116+ require(username.nonEmpty, " JDBC username is not configured" )
117+ // allow empty password
118+ require(authQuery.nonEmpty, " Query SQL is not configured" )
119+
120+ val query = authQuery.get.trim.toLowerCase
121+ // allow simple select query sql only, complex query like CTE is not allowed
122+ require(query.startsWith(" select" ), " Query SQL must start with 'SELECT'" )
123+ if (! query.contains(" where" )) {
124+ warn(" Query SQL does not contains 'WHERE' keyword" )
119125 }
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" );
131- }
132- if (! querySqlInLowerCase.contains(" ${username}" )) {
133- warn(" Query SQL does not contains \" ${username}\" placeholder" );
134- }
135- }
136-
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 = {
147-
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- }
126+ if (! query.contains(" ${username}" )) {
127+ warn(" Query SQL does not contains '${username}' placeholder" )
190128 }
191129 }
192130
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" )
131+ private def preparedQuery : String =
132+ SQL_PLACEHOLDER_REGEX .replaceAllIn(authQuery.get, " ?" )
201133
202- new HikariDataSource (hikariConfig)
203- }
134+ private def queryPlaceholders : Iterator [ String ] =
135+ SQL_PLACEHOLDER_REGEX .findAllMatchIn(authQuery.get).map(_.matched)
204136}
0 commit comments