Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0dc75fe
implement JDBC Authentication Method
bowenliang123 Aug 15, 2022
0e7f0ad
refactor config and init process.remove unused import.
bowenliang123 Aug 15, 2022
996f796
add unit test in JdbcAuthenticationProviderImplSuite
bowenliang123 Aug 15, 2022
49c18c2
update
bowenliang123 Aug 15, 2022
df4be56
update code style
bowenliang123 Aug 15, 2022
7025330
fix derby startup error in test
bowenliang123 Aug 15, 2022
46cc1dd
add config docs in docs/deployment/settings.md
bowenliang123 Aug 15, 2022
15176b2
fix import orders
bowenliang123 Aug 15, 2022
cd2c7c2
update settings.md config doc
bowenliang123 Aug 15, 2022
1dc4187
update settings.md config doc
bowenliang123 Aug 15, 2022
575301c
update options usage
bowenliang123 Aug 15, 2022
30974d1
update format
bowenliang123 Aug 15, 2022
3672919
fix ddl statement and remove truncate statement in test
bowenliang123 Aug 16, 2022
cdec206
more test cases
bowenliang123 Aug 16, 2022
653bc12
add more checks for query sql
bowenliang123 Aug 16, 2022
aeb19ce
update doc
bowenliang123 Aug 16, 2022
b9ffac3
Merge branch 'master' into feature-jdbc-auth-provider
bowenliang123 Aug 16, 2022
9885f81
add JDBC condition for getValidPasswordAuthMethod
bowenliang123 Aug 16, 2022
4ebe12e
add JDBC value to AuthTypes enum
bowenliang123 Aug 16, 2022
1c956df
update KyuubiAuthenticationFactorySuite
bowenliang123 Aug 16, 2022
5a0ac49
output password length only in checkConfigs
bowenliang123 Aug 16, 2022
3a4d5fe
update checkConfigs() signature
bowenliang123 Aug 16, 2022
a4fe582
refactor connection creation on using HikariDataSource in HikariCP. a…
bowenliang123 Aug 16, 2022
543c66c
prefer scala style string usage
bowenliang123 Aug 16, 2022
6765aff
changed to use in-memory derby db for test
bowenliang123 Aug 16, 2022
77f5f86
remove unuseful comment
bowenliang123 Aug 16, 2022
a9404fa
use {} for intercept
bowenliang123 Aug 16, 2022
6fc42bf
code styling
bowenliang123 Aug 16, 2022
e9af096
use clone instead of repeatly generating configs
bowenliang123 Aug 16, 2022
d5f43e0
remove unuseful logs for unrecognized placeholder error
bowenliang123 Aug 16, 2022
17403b3
cleanup docs
bowenliang123 Aug 17, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/deployment/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,13 @@ You can configure the Kyuubi properties in `$KYUUBI_HOME/conf/kyuubi-defaults.co

Key | Default | Meaning | Type | Since
--- | --- | --- | --- | ---
kyuubi.authentication|NONE|A comma separated list of client authentication types.<ul> <li>NOSASL: raw transport.</li> <li>NONE: no authentication check.</li> <li>KERBEROS: Kerberos/GSSAPI authentication.</li> <li>CUSTOM: User-defined authentication.</li> <li>LDAP: Lightweight Directory Access Protocol authentication.</li></ul> Note that: For KERBEROS, it is SASL/GSSAPI mechanism, and for NONE, CUSTOM and LDAP, they are all SASL/PLAIN mechanism. If only NOSASL is specified, the authentication will be NOSASL. For SASL authentication, KERBEROS and PLAIN auth type are supported at the same time, and only the first specified PLAIN auth type is valid.|seq|1.0.0
kyuubi.authentication|NONE|A comma separated list of client authentication types.<ul> <li>NOSASL: raw transport.</li> <li>NONE: no authentication check.</li> <li>KERBEROS: Kerberos/GSSAPI authentication.</li> <li>CUSTOM: User-defined authentication.</li> <li>JDBC: JDBC query authentication.</li> <li>LDAP: Lightweight Directory Access Protocol authentication.</li></ul> Note that: For KERBEROS, it is SASL/GSSAPI mechanism, and for NONE, CUSTOM and LDAP, they are all SASL/PLAIN mechanism. If only NOSASL is specified, the authentication will be NOSASL. For SASL authentication, KERBEROS and PLAIN auth type are supported at the same time, and only the first specified PLAIN auth type is valid.|seq|1.0.0
kyuubi.authentication.custom.class|&lt;undefined&gt;|User-defined authentication implementation of org.apache.kyuubi.service.authentication.PasswdAuthenticationProvider|string|1.3.0
kyuubi.authentication.jdbc.driver.class|&lt;undefined&gt;|Driver class name for JDBC Authentication Provider.|string|1.6.0
kyuubi.authentication.jdbc.password|&lt;undefined&gt;|Database password for JDBC Authentication Provider.|string|1.6.0
kyuubi.authentication.jdbc.query|&lt;undefined&gt;|Query SQL template with placeholders for JDBC Authentication Provider to execute. Authentication passes if at least one row fetched in the result set.Available placeholders are: <ul><li>`${username}`</li><li>`${password}`</li></ul>eg.: query sql `SELECT 1 FROM auth_table WHERE user=${username} AND passwd=MD5(CONCAT(salt,${password}));` will be prepared as: `SELECT 1 FROM auth_table WHERE user=? AND passwd=MD5(CONCAT(salt,?));` with value replacement of `username` and `password` in string type.|string|1.6.0
kyuubi.authentication.jdbc.url|&lt;undefined&gt;|JDBC URL for JDBC Authentication Provider.|string|1.6.0
kyuubi.authentication.jdbc.username|&lt;undefined&gt;|Database username for JDBC Authentication Provider.|string|1.6.0
kyuubi.authentication.ldap.base.dn|&lt;undefined&gt;|LDAP base DN.|string|1.0.0
kyuubi.authentication.ldap.domain|&lt;undefined&gt;|LDAP domain.|string|1.0.0
kyuubi.authentication.ldap.guidKey|uid|LDAP attribute name whose values are unique in this LDAP server.For example:uid or cn.|string|1.2.0
Expand Down
11 changes: 11 additions & 0 deletions kyuubi-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@
<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>

<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-minikdc</artifactId>
Expand All @@ -137,6 +142,12 @@
<artifactId>failureaccess</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,9 @@ object KyuubiConf {
" <li>NONE: no authentication check.</li>" +
" <li>KERBEROS: Kerberos/GSSAPI authentication.</li>" +
" <li>CUSTOM: User-defined authentication.</li>" +
" <li>LDAP: Lightweight Directory Access Protocol authentication.</li></ul>" +
" <li>JDBC: JDBC query authentication.</li>" +
" <li>LDAP: Lightweight Directory Access Protocol authentication.</li>" +
"</ul>" +
" Note that: For KERBEROS, it is SASL/GSSAPI mechanism," +
" and for NONE, CUSTOM and LDAP, they are all SASL/PLAIN mechanism." +
" If only NOSASL is specified, the authentication will be NOSASL." +
Expand Down Expand Up @@ -645,6 +647,51 @@ object KyuubiConf {
.stringConf
.createWithDefault("uid")

val AUTHENTICATION_JDBC_DRIVER: OptionalConfigEntry[String] =
buildConf("kyuubi.authentication.jdbc.driver.class")
.doc("Driver class name for JDBC Authentication Provider.")
.version("1.6.0")
.stringConf
.createOptional

val AUTHENTICATION_JDBC_URL: OptionalConfigEntry[String] =
buildConf("kyuubi.authentication.jdbc.url")
.doc("JDBC URL for JDBC Authentication Provider.")
.version("1.6.0")
.stringConf
.createOptional

val AUTHENTICATION_JDBC_USERNAME: OptionalConfigEntry[String] =
buildConf("kyuubi.authentication.jdbc.username")
.doc("Database username for JDBC Authentication Provider.")
.version("1.6.0")
.stringConf
.createOptional

val AUTHENTICATION_JDBC_PASSWORD: OptionalConfigEntry[String] =
buildConf("kyuubi.authentication.jdbc.password")
.doc("Database password for JDBC Authentication Provider.")
.version("1.6.0")
.stringConf
.createOptional

val AUTHENTICATION_JDBC_QUERY: OptionalConfigEntry[String] =
buildConf("kyuubi.authentication.jdbc.query")
.doc("Query SQL template with placeholders " +
"for JDBC Authentication Provider to execute. " +
"Authentication passes if at least one row fetched in the result set." +
"Available placeholders are: <ul>" +
"<li>`${username}`</li>" +
"<li>`${password}`</li></ul>" +
"eg.: query sql `SELECT 1 FROM auth_table WHERE user=${username} AND " +
"passwd=MD5(CONCAT(salt,${password}));` " +
"will be prepared as: `SELECT 1 FROM auth_table " +
"WHERE user=? AND passwd=MD5(CONCAT(salt,?));`" +
" with value replacement of `username` and `password` in string type.")
.version("1.6.0")
.stringConf
.createOptional

val DELEGATION_KEY_UPDATE_INTERVAL: ConfigEntry[Long] =
buildConf("kyuubi.delegation.key.update.interval")
.doc("unused yet")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ package org.apache.kyuubi.service.authentication

object AuthMethods extends Enumeration {
type AuthMethod = Value
val NONE, LDAP, CUSTOM = Value
val NONE, LDAP, JDBC, CUSTOM = Value
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ package org.apache.kyuubi.service.authentication
object AuthTypes extends Enumeration {
type AuthType = Value

val NOSASL, NONE, LDAP, KERBEROS, CUSTOM = Value
val NOSASL, NONE, LDAP, JDBC, KERBEROS, CUSTOM = Value
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ object AuthenticationProviderFactory {
conf: KyuubiConf): PasswdAuthenticationProvider = method match {
case AuthMethods.NONE => new AnonymousAuthenticationProviderImpl
case AuthMethods.LDAP => new LdapAuthenticationProviderImpl(conf)
case AuthMethods.JDBC => new JdbcAuthenticationProviderImpl(conf)
case AuthMethods.CUSTOM =>
val className = conf.get(KyuubiConf.AUTHENTICATION_CUSTOM_CLASS)
if (className.isEmpty) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.kyuubi.service.authentication

import java.sql.{Connection, PreparedStatement, Statement}
import java.util.Properties
import javax.security.sasl.AuthenticationException

import com.zaxxer.hikari.{HikariConfig, HikariDataSource}
import org.apache.commons.lang3.StringUtils

import org.apache.kyuubi.Logging
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.config.KyuubiConf._

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

private val driverClass = conf.get(AUTHENTICATION_JDBC_DRIVER)
private val jdbcUrl = conf.get(AUTHENTICATION_JDBC_URL)
private val jdbcUsername = conf.get(AUTHENTICATION_JDBC_USERNAME)
private val jdbcUserPassword = conf.get(AUTHENTICATION_JDBC_PASSWORD)
private val authQuerySql = conf.get(AUTHENTICATION_JDBC_QUERY)

private val SQL_PLACEHOLDER_REGEX = """\$\{.+?}""".r
private val USERNAME_SQL_PLACEHOLDER = "${username}"
private val PASSWORD_SQL_PLACEHOLDER = "${password}"

checkJdbcConfigs()

private[kyuubi] val hikariDataSource = getHikariDataSource

/**
* The authenticate method is called by the Kyuubi Server authentication layer
* to authenticate users for their requests.
* If a user is to be granted, return nothing/throw nothing.
* When a user is to be disallowed, throw an appropriate [[AuthenticationException]].
*
* @param user The username received over the connection request
* @param password The password received over the connection request
* @throws AuthenticationException When a user is found to be invalid by the implementation
*/
@throws[AuthenticationException]
override def authenticate(user: String, password: String): Unit = {
if (StringUtils.isBlank(user)) {
throw new AuthenticationException(s"Error validating, user is null" +
s" or contains blank space")
}

if (StringUtils.isBlank(password)) {
throw new AuthenticationException(s"Error validating, password is null" +
s" or contains blank space")
}

var connection: Connection = null
var queryStatement: PreparedStatement = null

try {
connection = hikariDataSource.getConnection

queryStatement = getAndPrepareQueryStatement(connection, user, password)

val resultSet = queryStatement.executeQuery()

if (resultSet == null || !resultSet.next()) {
// auth failed
throw new AuthenticationException(s"Password does not match or no such user. user:" +
s" $user , password length: ${password.length}")
}

// auth passed

} catch {
case e: AuthenticationException =>
throw e
case e: Exception =>
error("Cannot get user info", e);
throw e
} finally {
closeDbConnection(connection, queryStatement)
}
}

private def checkJdbcConfigs(): Unit = {
def configLog(config: String, value: String): String = s"JDBCAuthConfig: $config = '$value'"

debug(configLog("Driver Class", driverClass.orNull))
debug(configLog("JDBC URL", jdbcUrl.orNull))
debug(configLog("Database username", jdbcUsername.orNull))
debug(configLog("Database password length", jdbcUserPassword.getOrElse("").length.toString))
debug(configLog("Query SQL", authQuerySql.orNull))

// Check if JDBC parameters valid
if (driverClass.isEmpty) {
throw new IllegalArgumentException("JDBC driver class is not configured.")
}

if (jdbcUrl.isEmpty) {
throw new IllegalArgumentException("JDBC url is not configured")
}

if (jdbcUsername.isEmpty || jdbcUserPassword.isEmpty) {
throw new IllegalArgumentException("JDBC username or password is not configured")
}

// Check Query SQL
if (authQuerySql.isEmpty) {
throw new IllegalArgumentException("Query SQL is not configured")
}
val querySqlInLowerCase = authQuerySql.get.trim.toLowerCase
if (!querySqlInLowerCase.startsWith("select")) { // allow select query sql only
throw new IllegalArgumentException("Query SQL must start with \"SELECT\"");
}
if (!querySqlInLowerCase.contains("where")) {
warn("Query SQL does not contains \"WHERE\" keyword");
}
if (!querySqlInLowerCase.contains("${username}")) {
warn("Query SQL does not contains \"${username}\" placeholder");
}
}

private def getPlaceholderList(sql: String): List[String] = {
SQL_PLACEHOLDER_REGEX.findAllMatchIn(sql)
.map(m => m.matched)
.toList
}

private def getAndPrepareQueryStatement(
connection: Connection,
user: String,
password: String): PreparedStatement = {

val preparedSql: String = {
SQL_PLACEHOLDER_REGEX.replaceAllIn(authQuerySql.get, "?")
}
debug(s"prepared auth query sql: $preparedSql")

val stmt = connection.prepareStatement(preparedSql)
stmt.setMaxRows(1) // minimum result size required for authentication

// Extract placeholder list and fill parameters to placeholders
val placeholderList: List[String] = getPlaceholderList(authQuerySql.get)
for (i <- placeholderList.indices) {
val param = placeholderList(i) match {
case USERNAME_SQL_PLACEHOLDER => user
case PASSWORD_SQL_PLACEHOLDER => password
case otherPlaceholder =>
throw new IllegalArgumentException(
s"Unrecognized Placeholder In Query SQL: $otherPlaceholder")
}

stmt.setString(i + 1, param)
}

stmt
}

private def closeDbConnection(connection: Connection, statement: Statement): Unit = {
if (statement != null && !statement.isClosed) {
try {
statement.close()
} catch {
case e: Exception =>
error("Cannot close PreparedStatement to auth database ", e)
}
}

if (connection != null && !connection.isClosed) {
try {
connection.close()
} catch {
case e: Exception =>
error("Cannot close connection to auth database ", e)
}
}
}

private def getHikariDataSource: HikariDataSource = {
val datasourceProperties = new Properties()
val hikariConfig = new HikariConfig(datasourceProperties)
hikariConfig.setDriverClassName(driverClass.orNull)
hikariConfig.setJdbcUrl(jdbcUrl.orNull)
hikariConfig.setUsername(jdbcUsername.orNull)
hikariConfig.setPassword(jdbcUserPassword.orNull)
hikariConfig.setPoolName("jdbc-auth-pool")

new HikariDataSource(hikariConfig)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class KyuubiAuthenticationFactory(conf: KyuubiConf, isServer: Boolean = true) ex
debug(authTypes)
if (none) AuthMethods.NONE
else if (authTypes.contains(LDAP)) AuthMethods.LDAP
else if (authTypes.contains(JDBC)) AuthMethods.JDBC
else if (authTypes.contains(CUSTOM)) AuthMethods.CUSTOM
else throw new IllegalArgumentException("No valid Password Auth detected")
}
Expand Down
Loading