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]