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
24- import  com .zaxxer .hikari .{ HikariConfig ,  HikariDataSource } 
24+ import  com .zaxxer .hikari .util . DriverDataSource 
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  =  """ \$\{.+?}""" 
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" 
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" 
9697    }
9798  }
9899
@@ -101,104 +102,34 @@ class JdbcAuthenticationProviderImpl(conf: KyuubiConf) extends PasswdAuthenticat
101102
102103    debug(configLog(" Driver Class" 
103104    debug(configLog(" JDBC URL" 
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 " 
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 " 
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}
0 commit comments