This is an automated email from the ASF dual-hosted git repository.

wangzhen pushed a commit to branch branch-1.9
in repository https://gitbox.apache.org/repos/asf/kyuubi.git


The following commit(s) were added to refs/heads/branch-1.9 by this push:
     new 63b67a56f [KYUUBI #6216] Support to deny some client ips to make 
connection
63b67a56f is described below

commit 63b67a56f12f97a13343d65318cffbdafada4972
Author: wforget <[email protected]>
AuthorDate: Sun Apr 7 16:32:00 2024 +0800

    [KYUUBI #6216] Support to deny some client ips to make connection
    
    # :mag: Description
    ## Issue References ๐Ÿ”—
    
    This pull request fixes #6216
    
    ## Describe Your Solution ๐Ÿ”ง
    
    Similar to #4540, sometimes we need to quickly deny requests from some 
clients, so I added `kyuubi.server.limit.connections.ip.deny.list` to limit 
client ips.
    
    ## Types of changes :bookmark:
    
    - [ ] Bugfix (non-breaking change which fixes an issue)
    - [X] New feature (non-breaking change which adds functionality)
    - [ ] Breaking change (fix or feature that would cause existing 
functionality to change)
    
    ## Test Plan ๐Ÿงช
    
    #### Behavior Without This Pull Request :coffin:
    
    #### Behavior With This Pull Request :tada:
    
    #### Related Unit Tests
    
    SessionLimiterSuite.test("test session limiter with ip deny list")
    ---
    
    # Checklist ๐Ÿ“
    
    - [X] This patch was not authored or co-authored using [Generative 
Tooling](https://www.apache.org/legal/generative-tooling.html)
    
    **Be nice. Be informative.**
    
    Closes #6217 from wForget/KYUUBI-6216.
    
    Closes #6216
    
    e0a058c1d [wforget] update ctl
    f66426eda [wforget] typo
    5c5308ad8 [wforget] typo
    959242e9b [wforget] Support to deny some client ips to make connection
    
    Authored-by: wforget <[email protected]>
    Signed-off-by: wforget <[email protected]>
    (cherry picked from commit a4c3c0b75912ee60d1186c7531bf1d0da8ddda01)
    Signed-off-by: wforget <[email protected]>
---
 docs/configuration/settings.md                     |  1 +
 .../org/apache/kyuubi/config/KyuubiConf.scala      |  9 +++++++
 .../ctl/cmd/refresh/RefreshConfigCommand.scala     |  4 ++-
 .../apache/kyuubi/ctl/opt/AdminCommandLine.scala   |  2 +-
 .../kyuubi/ctl/AdminControlCliArgumentsSuite.scala | 11 +++++++-
 .../org/apache/kyuubi/client/AdminRestApi.java     |  5 ++++
 .../org/apache/kyuubi/server/KyuubiServer.scala    |  8 ++++++
 .../kyuubi/server/api/v1/AdminResource.scala       | 19 ++++++++++++++
 .../kyuubi/session/KyuubiSessionManager.scala      | 26 +++++++++++++++----
 .../org/apache/kyuubi/session/SessionLimiter.scala | 30 +++++++++++++++++++---
 .../kyuubi/session/SessionLimiterSuite.scala       | 15 +++++++++++
 11 files changed, 119 insertions(+), 11 deletions(-)

diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md
index 2ea09228f..fec770917 100644
--- a/docs/configuration/settings.md
+++ b/docs/configuration/settings.md
@@ -421,6 +421,7 @@ You can configure the Kyuubi properties in 
`$KYUUBI_HOME/conf/kyuubi-defaults.co
 | kyuubi.server.limit.batch.connections.per.user           | &lt;undefined&gt; 
| Maximum kyuubi server batch connections per user. Any user exceeding this 
limit will not be allowed to connect.                                           
                                                                                
                              | int      | 1.7.0 |
 | kyuubi.server.limit.batch.connections.per.user.ipaddress | &lt;undefined&gt; 
| Maximum kyuubi server batch connections per user:ipaddress combination. Any 
user-ipaddress exceeding this limit will not be allowed to connect.             
                                                                                
                            | int      | 1.7.0 |
 | kyuubi.server.limit.client.fetch.max.rows                | &lt;undefined&gt; 
| Max rows limit for getting result row set operation. If the max rows 
specified by client-side is larger than the limit, request will fail directly.  
                                                                                
                                   | int      | 1.8.0 |
+| kyuubi.server.limit.connections.ip.deny.list                                
|| The client ip in the deny list will be denied to connect to kyuubi server.   
                                                                                
                                                                                
                           | set      | 1.9.1 |
 | kyuubi.server.limit.connections.per.ipaddress            | &lt;undefined&gt; 
| Maximum kyuubi server connections per ipaddress. Any user exceeding this 
limit will not be allowed to connect.                                           
                                                                                
                               | int      | 1.6.0 |
 | kyuubi.server.limit.connections.per.user                 | &lt;undefined&gt; 
| Maximum kyuubi server connections per user. Any user exceeding this limit 
will not be allowed to connect.                                                 
                                                                                
                              | int      | 1.6.0 |
 | kyuubi.server.limit.connections.per.user.ipaddress       | &lt;undefined&gt; 
| Maximum kyuubi server connections per user:ipaddress combination. Any 
user-ipaddress exceeding this limit will not be allowed to connect.             
                                                                                
                                  | int      | 1.6.0 |
diff --git 
a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala 
b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
index 8305ddfd7..f06647ab7 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/config/KyuubiConf.scala
@@ -2857,6 +2857,15 @@ object KyuubiConf {
       .toSet()
       .createWithDefault(Set.empty)
 
+  val SERVER_LIMIT_CONNECTIONS_IP_DENY_LIST: ConfigEntry[Set[String]] =
+    buildConf("kyuubi.server.limit.connections.ip.deny.list")
+      .doc("The client ip in the deny list will be denied to connect to kyuubi 
server.")
+      .version("1.9.1")
+      .serverOnly
+      .stringConf
+      .toSet()
+      .createWithDefault(Set.empty)
+
   val SERVER_LIMIT_BATCH_CONNECTIONS_PER_USER: OptionalConfigEntry[Int] =
     buildConf("kyuubi.server.limit.batch.connections.per.user")
       .doc("Maximum kyuubi server batch connections per user." +
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/refresh/RefreshConfigCommand.scala
 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/refresh/RefreshConfigCommand.scala
index 1cda224df..a7704ca47 100644
--- 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/refresh/RefreshConfigCommand.scala
+++ 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/cmd/refresh/RefreshConfigCommand.scala
@@ -21,7 +21,7 @@ import org.apache.kyuubi.KyuubiException
 import org.apache.kyuubi.client.AdminRestApi
 import org.apache.kyuubi.ctl.RestClientFactory.withKyuubiRestClient
 import org.apache.kyuubi.ctl.cmd.AdminCtlCommand
-import 
org.apache.kyuubi.ctl.cmd.refresh.RefreshConfigCommandConfigType.{DENY_USERS, 
HADOOP_CONF, KUBERNETES_CONF, UNLIMITED_USERS, USER_DEFAULTS_CONF}
+import 
org.apache.kyuubi.ctl.cmd.refresh.RefreshConfigCommandConfigType.{DENY_IPS, 
DENY_USERS, HADOOP_CONF, KUBERNETES_CONF, UNLIMITED_USERS, USER_DEFAULTS_CONF}
 import org.apache.kyuubi.ctl.opt.CliConfig
 import org.apache.kyuubi.ctl.util.{Tabulator, Validator}
 
@@ -39,6 +39,7 @@ class RefreshConfigCommand(cliConfig: CliConfig) extends 
AdminCtlCommand[String]
         case KUBERNETES_CONF => adminRestApi.refreshKubernetesConf()
         case UNLIMITED_USERS => adminRestApi.refreshUnlimitedUsers()
         case DENY_USERS => adminRestApi.refreshDenyUsers()
+        case DENY_IPS => adminRestApi.refreshDenyIps()
         case configType => throw new KyuubiException(s"Invalid config 
type:$configType")
       }
     }
@@ -54,4 +55,5 @@ object RefreshConfigCommandConfigType {
   final val KUBERNETES_CONF = "kubernetesConf"
   final val UNLIMITED_USERS = "unlimitedUsers"
   final val DENY_USERS = "denyUsers"
+  final val DENY_IPS = "denyIps"
 }
diff --git 
a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala 
b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala
index c7e367405..b6bcef0b2 100644
--- a/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala
+++ b/kyuubi-ctl/src/main/scala/org/apache/kyuubi/ctl/opt/AdminCommandLine.scala
@@ -109,6 +109,6 @@ object AdminCommandLine extends CommonCommandLine {
           .action((v, c) => c.copy(adminConfigOpts = 
c.adminConfigOpts.copy(configType = v)))
           .text("The valid config type can be one of the following: " +
             s"$HADOOP_CONF, $USER_DEFAULTS_CONF, $KUBERNETES_CONF, " +
-            s"$UNLIMITED_USERS, $DENY_USERS."))
+            s"$UNLIMITED_USERS, $DENY_USERS, $DENY_IPS."))
   }
 }
diff --git 
a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala
 
b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala
index 52a2796f4..fba36f85e 100644
--- 
a/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala
+++ 
b/kyuubi-ctl/src/test/scala/org/apache/kyuubi/ctl/AdminControlCliArgumentsSuite.scala
@@ -101,6 +101,15 @@ class AdminControlCliArgumentsSuite extends KyuubiFunSuite 
with TestPrematureExi
     assert(opArgs5.cliConfig.resource === ControlObject.CONFIG)
     assert(opArgs5.cliConfig.adminConfigOpts.configType === DENY_USERS)
 
+    args = Array(
+      "refresh",
+      "config",
+      "denyIps")
+    val opArgs6 = new AdminControlCliArguments(args)
+    assert(opArgs6.cliConfig.action === ControlAction.REFRESH)
+    assert(opArgs6.cliConfig.resource === ControlObject.CONFIG)
+    assert(opArgs6.cliConfig.adminConfigOpts.configType === DENY_IPS)
+
     args = Array(
       "refresh",
       "config",
@@ -183,7 +192,7 @@ class AdminControlCliArgumentsSuite extends KyuubiFunSuite 
with TestPrematureExi
          |     Refresh the resource.
          |Command: refresh config [<configType>]
          |     Refresh the config with specified type.
-         |  <configType>             The valid config type can be one of the 
following: $HADOOP_CONF, $USER_DEFAULTS_CONF, $KUBERNETES_CONF, 
$UNLIMITED_USERS, $DENY_USERS.
+         |  <configType>             The valid config type can be one of the 
following: $HADOOP_CONF, $USER_DEFAULTS_CONF, $KUBERNETES_CONF, 
$UNLIMITED_USERS, $DENY_USERS, $DENY_IPS.
          |
          |  -h, --help               Show help message and exit.""".stripMargin
     // scalastyle:on
diff --git 
a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java 
b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java
index 834b508de..002f6b46d 100644
--- 
a/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java
+++ 
b/kyuubi-rest-client/src/main/java/org/apache/kyuubi/client/AdminRestApi.java
@@ -60,6 +60,11 @@ public class AdminRestApi {
     return this.getClient().post(path, null, client.getAuthHeader());
   }
 
+  public String refreshDenyIps() {
+    String path = String.format("%s/%s", API_BASE_PATH, "refresh/deny_ips");
+    return this.getClient().post(path, null, client.getAuthHeader());
+  }
+
   public String deleteEngine(
       String engineType, String shareLevel, String subdomain, String 
hs2ProxyUser) {
     Map<String, Object> params = new HashMap<>();
diff --git 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
index 453ae0b79..b5f135339 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiServer.scala
@@ -161,6 +161,14 @@ object KyuubiServer extends Logging {
     val refreshedDenyUsers = sessionMgr.getDenyUsers
     info(s"Refreshed deny users from $existingDenyUsers to 
$refreshedDenyUsers")
   }
+
+  private[kyuubi] def refreshDenyIps(): Unit = synchronized {
+    val sessionMgr = 
kyuubiServer.backendService.sessionManager.asInstanceOf[KyuubiSessionManager]
+    val existingDenyIps = sessionMgr.getDenyIps
+    sessionMgr.refreshDenyIps(KyuubiConf().loadFileDefaults())
+    val refreshedDenyIps = sessionMgr.getDenyIps
+    info(s"Refreshed deny client ips from $existingDenyIps to 
$refreshedDenyIps")
+  }
 }
 
 class KyuubiServer(name: String) extends Serverable(name) {
diff --git 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala
 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala
index ca35a1b6b..e31c792c3 100644
--- 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala
+++ 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/AdminResource.scala
@@ -141,6 +141,25 @@ private[v1] class AdminResource extends ApiRequestContext 
with Logging {
     Response.ok(s"Refresh the deny users successfully.").build()
   }
 
+  @ApiResponse(
+    responseCode = "200",
+    content = Array(new Content(mediaType = MediaType.APPLICATION_JSON)),
+    description = "refresh the deny ips")
+  @POST
+  @Path("refresh/deny_ips")
+  def refreshDenyIp(): Response = {
+    val userName = fe.getSessionUser(Map.empty[String, String])
+    val ipAddress = fe.getIpAddress
+    info(s"Receive refresh deny ips request from $userName/$ipAddress")
+    if (!fe.isAdministrator(userName)) {
+      throw new NotAllowedException(
+        s"$userName is not allowed to refresh the deny ips")
+    }
+    info(s"Reloading deny ips")
+    KyuubiServer.refreshDenyIps()
+    Response.ok(s"Refresh the deny ips successfully.").build()
+  }
+
   @ApiResponse(
     responseCode = "200",
     content = Array(new Content(
diff --git 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionManager.scala
 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionManager.scala
index 924145882..2297cc02c 100644
--- 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionManager.scala
+++ 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/KyuubiSessionManager.scala
@@ -338,12 +338,14 @@ class KyuubiSessionManager private (name: String) extends 
SessionManager(name) {
     val userUnlimitedList =
       conf.get(SERVER_LIMIT_CONNECTIONS_USER_UNLIMITED_LIST).filter(_.nonEmpty)
     val userDenyList = 
conf.get(SERVER_LIMIT_CONNECTIONS_USER_DENY_LIST).filter(_.nonEmpty)
+    val ipDenyList = 
conf.get(SERVER_LIMIT_CONNECTIONS_IP_DENY_LIST).filter(_.nonEmpty)
     limiter = applySessionLimiter(
       userLimit,
       ipAddressLimit,
       userIpAddressLimit,
       userUnlimitedList,
-      userDenyList)
+      userDenyList,
+      ipDenyList)
 
     val userBatchLimit = 
conf.get(SERVER_LIMIT_BATCH_CONNECTIONS_PER_USER).getOrElse(0)
     val ipAddressBatchLimit = 
conf.get(SERVER_LIMIT_BATCH_CONNECTIONS_PER_IPADDRESS).getOrElse(0)
@@ -354,7 +356,8 @@ class KyuubiSessionManager private (name: String) extends 
SessionManager(name) {
       ipAddressBatchLimit,
       userIpAddressBatchLimit,
       userUnlimitedList,
-      userDenyList)
+      userDenyList,
+      ipDenyList)
   }
 
   private[kyuubi] def getUnlimitedUsers: Set[String] = {
@@ -378,19 +381,32 @@ class KyuubiSessionManager private (name: String) extends 
SessionManager(name) {
     batchLimiter.foreach(SessionLimiter.resetDenyUsers(_, denyUsers))
   }
 
+  private[kyuubi] def getDenyIps: Set[String] = {
+    
limiter.orElse(batchLimiter).map(SessionLimiter.getDenyIps).getOrElse(Set.empty)
+  }
+
+  private[kyuubi] def refreshDenyIps(conf: KyuubiConf): Unit = {
+    val denyIps = 
conf.get(SERVER_LIMIT_CONNECTIONS_IP_DENY_LIST).filter(_.nonEmpty)
+    limiter.foreach(SessionLimiter.resetDenyIps(_, denyIps))
+    batchLimiter.foreach(SessionLimiter.resetDenyIps(_, denyIps))
+  }
+
   private def applySessionLimiter(
       userLimit: Int,
       ipAddressLimit: Int,
       userIpAddressLimit: Int,
       userUnlimitedList: Set[String],
-      userDenyList: Set[String]): Option[SessionLimiter] = {
-    if (Seq(userLimit, ipAddressLimit, userIpAddressLimit).exists(_ > 0) || 
userDenyList.nonEmpty) {
+      userDenyList: Set[String],
+      ipDenyList: Set[String]): Option[SessionLimiter] = {
+    if (Seq(userLimit, ipAddressLimit, userIpAddressLimit).exists(_ > 0) ||
+      userDenyList.nonEmpty || ipDenyList.nonEmpty) {
       Some(SessionLimiter(
         userLimit,
         ipAddressLimit,
         userIpAddressLimit,
         userUnlimitedList,
-        userDenyList))
+        userDenyList,
+        ipDenyList))
     } else {
       None
     }
diff --git 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/SessionLimiter.scala 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/SessionLimiter.scala
index 8a1ebedf1..688154d00 100644
--- 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/session/SessionLimiter.scala
+++ 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/session/SessionLimiter.scala
@@ -107,7 +107,8 @@ class SessionLimiterWithAccessControlListImpl(
     ipAddressLimit: Int,
     userIpAddressLimit: Int,
     var unlimitedUsers: Set[String],
-    var denyUsers: Set[String])
+    var denyUsers: Set[String],
+    var denyIps: Set[String])
   extends SessionLimiterImpl(userLimit, ipAddressLimit, userIpAddressLimit) {
   override def increment(userIpAddress: UserIpAddress): Unit = {
     val user = userIpAddress.user
@@ -116,6 +117,12 @@ class SessionLimiterWithAccessControlListImpl(
         s"Connection denied because the user is in the deny user list. (user: 
$user)"
       throw KyuubiSQLException(errorMsg)
     }
+    val ip = userIpAddress.ipAddress
+    if (StringUtils.isNotBlank(ip) && denyIps.contains(ip)) {
+      val errorMsg =
+        s"Connection denied because the client ip is in the deny ip list. 
(ipAddress: $ip)"
+      throw KyuubiSQLException(errorMsg)
+    }
 
     if (!unlimitedUsers.contains(user)) {
       super.increment(userIpAddress)
@@ -129,6 +136,10 @@ class SessionLimiterWithAccessControlListImpl(
   private[kyuubi] def setDenyUsers(denyUsers: Set[String]): Unit = {
     this.denyUsers = denyUsers
   }
+
+  private[kyuubi] def setDenyIps(denyIps: Set[String]): Unit = {
+    this.denyIps = denyIps
+  }
 }
 
 object SessionLimiter {
@@ -138,13 +149,15 @@ object SessionLimiter {
       ipAddressLimit: Int,
       userIpAddressLimit: Int,
       unlimitedUsers: Set[String] = Set.empty,
-      denyUsers: Set[String] = Set.empty): SessionLimiter = {
+      denyUsers: Set[String] = Set.empty,
+      denyIps: Set[String] = Set.empty): SessionLimiter = {
     new SessionLimiterWithAccessControlListImpl(
       userLimit,
       ipAddressLimit,
       userIpAddressLimit,
       unlimitedUsers,
-      denyUsers)
+      denyUsers,
+      denyIps)
   }
 
   def resetUnlimitedUsers(limiter: SessionLimiter, unlimitedUsers: 
Set[String]): Unit =
@@ -168,4 +181,15 @@ object SessionLimiter {
     case l: SessionLimiterWithAccessControlListImpl => l.denyUsers
     case _ => Set.empty
   }
+
+  def resetDenyIps(limiter: SessionLimiter, denyIps: Set[String]): Unit =
+    limiter match {
+      case l: SessionLimiterWithAccessControlListImpl => l.setDenyIps(denyIps)
+      case _ =>
+    }
+
+  def getDenyIps(limiter: SessionLimiter): Set[String] = limiter match {
+    case l: SessionLimiterWithAccessControlListImpl => l.denyIps
+    case _ => Set.empty
+  }
 }
diff --git 
a/kyuubi-server/src/test/scala/org/apache/kyuubi/session/SessionLimiterSuite.scala
 
b/kyuubi-server/src/test/scala/org/apache/kyuubi/session/SessionLimiterSuite.scala
index 775239f9b..47c33d63e 100644
--- 
a/kyuubi-server/src/test/scala/org/apache/kyuubi/session/SessionLimiterSuite.scala
+++ 
b/kyuubi-server/src/test/scala/org/apache/kyuubi/session/SessionLimiterSuite.scala
@@ -152,6 +152,21 @@ class SessionLimiterSuite extends KyuubiFunSuite {
       "Connection denied because the user is in the deny user list. (user: 
user002)"))
   }
 
+  test("test session limiter with ip deny list") {
+    val ipAddress = "127.0.0.1"
+    val denyIps = Set(ipAddress)
+    val limiter =
+      SessionLimiter(100, 100, 100, Set.empty, Set.empty, denyIps)
+
+    val caught = intercept[KyuubiSQLException] {
+      val userIpAddress = UserIpAddress("user001", ipAddress)
+      limiter.increment(userIpAddress)
+    }
+
+    assert(caught.getMessage.equals(
+      "Connection denied because the client ip is in the deny ip list. 
(ipAddress: 127.0.0.1)"))
+  }
+
   test("test refresh unlimited users and deny users") {
     val random: Random = new Random()
     val latch = new CountDownLatch(600)

Reply via email to