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

cecemei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new fd3e83cfc6e fix: Close connection when JDBC authentication fails 
(#20594)
fd3e83cfc6e is described below

commit fd3e83cfc6e2a5aee8df8e716e143354dbcc76f4
Author: Cece Mei <[email protected]>
AuthorDate: Wed Oct 14 22:43:04 2026 -0700

    fix: Close connection when JDBC authentication fails (#20594)
    
    * close
    
    * doc
    
    * test
    
    * test
---
 docs/operations/security-overview.md               |  2 +-
 .../org/apache/druid/sql/avatica/DruidMeta.java    |  6 ++
 .../druid/sql/avatica/DruidAvaticaHandlerTest.java | 80 ++++++++++++++++++++++
 3 files changed, 87 insertions(+), 1 deletion(-)

diff --git a/docs/operations/security-overview.md 
b/docs/operations/security-overview.md
index 82f4ad56310..b3cdb346c11 100644
--- a/docs/operations/security-overview.md
+++ b/docs/operations/security-overview.md
@@ -47,7 +47,7 @@ The following recommendations apply to the network where 
Druid runs:
   - Restrict access from untrusted networks
   - Create an allow list of specific APIs that your users need to access
   - Implement account lockout and throttling features.
-* When possible, use firewall and other network layer filtering to only expose 
Druid services and ports specifically required for your use case. For example, 
only expose Broker ports to downstream applications that execute queries. You 
can limit access to a specific IP address or IP range to further tighten and 
enhance security.
+* When possible, use firewall and other network layer filtering to only expose 
Druid services and ports specifically required for your use case. For example, 
only expose Broker ports to downstream applications that execute queries. You 
can limit access to a specific IP address or IP range to further tighten and 
enhance security. This is especially important for the Avatica JDBC endpoint 
(`/druid/v2/sql/avatica/`), which authenticates within the JDBC protocol 
itself, so network-level rest [...]
 
 The following recommendation applies to Druid's authorization and 
authentication model:
 * Only grant `WRITE` permissions to any `DATASOURCE` to trusted users. Druid's 
trust model assumes those users have the same privileges as the operating 
system user that runs the web console process. Additionally, users with `WRITE` 
permissions can make changes to datasources and they have access to both task 
and supervisor update (POST) APIs which may affect ingestion.
diff --git a/sql/src/main/java/org/apache/druid/sql/avatica/DruidMeta.java 
b/sql/src/main/java/org/apache/druid/sql/avatica/DruidMeta.java
index 74524b71eef..3b7b0e91612 100644
--- a/sql/src/main/java/org/apache/druid/sql/avatica/DruidMeta.java
+++ b/sql/src/main/java/org/apache/druid/sql/avatica/DruidMeta.java
@@ -335,6 +335,12 @@ public class DruidMeta extends MetaImpl
       return authenticationResult;
     }
 
+    // Credentials are fixed at openConnection() and cannot be retried under 
the same
+    // connection id, so a failure here is terminal for this connection. Close 
it immediately
+    // rather than leaving it to occupy a connection slot until it times out, 
since it can
+    // never be reclaimed by closeIfEmpty() once a statement exists on it.
+    closeConnection(new ConnectionHandle(druidConnection.getConnectionId()));
+
     // Throw an error. Use the same text that will appear if the user is denied
     // access to any resource within the query. See mapException(). This 
consistency
     // is helpful for users, essential for the ITs, which don't know about the 
two
diff --git 
a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java 
b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java
index 50b84cdd9c4..23cefc01500 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java
@@ -71,8 +71,11 @@ import org.apache.druid.server.initialization.ServerConfig;
 import org.apache.druid.server.log.RequestLogger;
 import org.apache.druid.server.log.TestRequestLogger;
 import org.apache.druid.server.metrics.NoopServiceEmitter;
+import org.apache.druid.server.security.AllowAllAuthenticator;
 import org.apache.druid.server.security.AuthConfig;
 import org.apache.druid.server.security.AuthTestUtils;
+import org.apache.druid.server.security.AuthenticationResult;
+import org.apache.druid.server.security.Authenticator;
 import org.apache.druid.server.security.AuthenticatorMapper;
 import org.apache.druid.server.security.AuthorizerMapper;
 import org.apache.druid.server.security.Escalator;
@@ -1107,6 +1110,83 @@ public class DruidAvaticaHandlerTest extends 
CalciteTestBase
     }
   }
 
+  @Test
+  public void 
testStatementExecutionFailsAuthenticationAndFreesConnectionSlot() throws 
Exception
+  {
+    final AvaticaServerConfig config = new AvaticaServerConfig();
+    config.maxConnections = 1;
+    config.maxStatementsPerConnection = STATEMENT_LIMIT;
+
+    final ScheduledExecutorService exec = 
Execs.scheduledSingleThreaded("testCreateStatementAuthFailure");
+    final List<Authenticator> authenticators = ImmutableList.of(
+        new AllowAllAuthenticator()
+        {
+          @Override
+          public AuthenticationResult authenticateJDBCContext(Map<String, 
Object> context)
+          {
+            if (!"druid".equals(context.get("password"))) {
+              return null;
+            }
+            return new AuthenticationResult((String) context.get("user"), 
AuthConfig.ALLOW_ALL_NAME, null, null);
+          }
+        }
+    );
+
+    final DruidMeta druidMeta = new DruidMeta(
+        makeStatementFactory(),
+        DefaultQueryConfig.NIL,
+        config,
+        new ErrorHandler(new ServerConfig()),
+        exec,
+        authenticators,
+        new ResultFetcherFactory(config.fetchTimeoutMs)
+    );
+
+    try (final ServerWrapper authServer = new ServerWrapper(druidMeta)) {
+      try (final Connection badConnection = 
authServer.getConnection("regularUser", "wrong-password")) {
+        // openConnection() and createStatement() succeed even with bad 
credentials;
+        // authentication is deferred until the statement is actually executed.
+        final Statement badStatement = badConnection.createStatement();
+        final AvaticaSqlException ex = Assertions.assertThrows(
+            AvaticaSqlException.class,
+            () -> badStatement.executeQuery("SELECT COUNT(*) AS cnt FROM 
druid.foo")
+        );
+        Assertions.assertTrue(ex.getMessage().contains("Unauthorized"));
+      }
+
+      // The failed connection must not have consumed the only available slot: 
a correctly
+      // authenticated connection should succeed immediately afterward, with 
no need to wait
+      // for the idle timeout or for another connection attempt to trigger 
closeIfEmpty() eviction.
+      try (final Connection goodConnection = 
authServer.getConnection("regularUser", "druid")) {
+        // Statement/ResultSet are kept open (then closed in the finally block 
below) because
+        // closing them immediately would let closeIfEmpty() reclaim the 
connection's slot before
+        // the sanity check below runs.
+        final Statement statement = goodConnection.createStatement();
+        final ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) AS 
cnt FROM druid.foo");
+        try {
+          Assertions.assertTrue(resultSet.next());
+
+          // Sanity check the other direction: once the single slot is 
legitimately occupied by a
+          // connection with a real statement on it, a new connection attempt 
must still be blocked.
+          // The fix only frees slots on authentication failure; it must not 
otherwise weaken
+          // maxConnections enforcement.
+          final AvaticaClientRuntimeException blockedEx = 
Assertions.assertThrows(
+              AvaticaClientRuntimeException.class,
+              () -> authServer.getConnection("regularUser", "druid")
+          );
+          Assertions.assertTrue(blockedEx.getMessage().contains("Too many 
connections"));
+        }
+        finally {
+          resultSet.close();
+          statement.close();
+        }
+      }
+    }
+    finally {
+      exec.shutdown();
+    }
+  }
+
   @Test
   public void testConnectionsCloseStatements() throws SQLException
   {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to