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

rubenada pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite-avatica.git


The following commit(s) were added to refs/heads/main by this push:
     new 0187d7e32 [CALCITE-7745] Implement a mechanism to restrict the 
connection properties that can be set by the caller
0187d7e32 is described below

commit 0187d7e323f2e37c79cc248ac9bc34b9953ac24b
Author: Ruben Quesada Lopez <[email protected]>
AuthorDate: Wed Aug 26 11:17:45 2026 +0100

    [CALCITE-7745] Implement a mechanism to restrict the connection properties 
that can be set by the caller
---
 .../jdbc/ForbiddenConnectionPropertyException.java |  71 ++++++++++++
 .../org/apache/calcite/avatica/jdbc/JdbcMeta.java  |  77 +++++++++++++
 .../apache/calcite/avatica/jdbc/JdbcMetaTest.java  | 121 +++++++++++++++++++++
 3 files changed, 269 insertions(+)

diff --git 
a/server/src/main/java/org/apache/calcite/avatica/jdbc/ForbiddenConnectionPropertyException.java
 
b/server/src/main/java/org/apache/calcite/avatica/jdbc/ForbiddenConnectionPropertyException.java
new file mode 100644
index 000000000..d43f6bfe3
--- /dev/null
+++ 
b/server/src/main/java/org/apache/calcite/avatica/jdbc/ForbiddenConnectionPropertyException.java
@@ -0,0 +1,71 @@
+/*
+ * 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.calcite.avatica.jdbc;
+
+/**
+ * Thrown when {@link JdbcMeta#openConnection} rejects a client-supplied
+ * connection property because the server-side configuration forbids it.
+ *
+ * <p>Rejection is driven by
+ * {@link JdbcMeta#CLIENT_PROPERTIES_DENYLIST_KEY} and
+ * {@link JdbcMeta#CLIENT_PROPERTIES_ALLOWLIST_KEY}.
+ */
+public class ForbiddenConnectionPropertyException extends RuntimeException {
+
+  private static final long serialVersionUID = 1L;
+
+  /** Which rule rejected a property. */
+  public enum Rule {
+    DENYLIST(JdbcMeta.CLIENT_PROPERTIES_DENYLIST_KEY),
+    ALLOWLIST(JdbcMeta.CLIENT_PROPERTIES_ALLOWLIST_KEY);
+
+    private final String configKey;
+
+    Rule(String configKey) {
+      this.configKey = configKey;
+    }
+
+    /** The server-side configuration key that drives this rule. */
+    public String configKey() {
+      return configKey;
+    }
+  }
+
+  private final String propertyName;
+  private final Rule rule;
+
+  public ForbiddenConnectionPropertyException(String propertyName, Rule rule) {
+    super("Client-supplied connection property '" + propertyName
+        + "' is forbidden by the server's "
+        + rule.name().toLowerCase(java.util.Locale.ROOT)
+        + " (" + rule.configKey() + ")");
+    this.propertyName = propertyName;
+    this.rule = rule;
+  }
+
+  /** The name of the client-supplied property that was rejected. */
+  public String getPropertyName() {
+    return propertyName;
+  }
+
+  /** Which rule rejected the property. */
+  public Rule getRule() {
+    return rule;
+  }
+}
+
+// End ForbiddenConnectionPropertyException.java
diff --git a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java 
b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
index 363ea4bac..b742deb39 100644
--- a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
+++ b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
@@ -61,10 +61,12 @@
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -79,6 +81,26 @@ public class JdbcMeta implements ProtobufMeta {
 
   private static final String STMT_CACHE_KEY_BASE = "avatica.statementcache";
 
+  /** JVM system property naming a comma-separated list of client
+   * connection-property names that are always rejected. Checked before
+   * {@link #CLIENT_PROPERTIES_ALLOWLIST_KEY}. Unset by default; when unset,
+   * no property is rejected on this basis.
+   *
+   * <p>Configured on the server JVM (for example
+   * {@code 
-Davatica.server.connection.properties.denylist=allowLoadLocalInfile}). */
+  public static final String CLIENT_PROPERTIES_DENYLIST_KEY =
+      "avatica.server.connection.properties.denylist";
+
+  /** JVM system property naming a comma-separated list of the only client
+   * connection-property names that are accepted. Applied after
+   * {@link #CLIENT_PROPERTIES_DENYLIST_KEY}. Unset by default; when unset,
+   * any property that survives the denylist check is accepted.
+   *
+   * <p>Configured on the server JVM (for example
+   * {@code 
-Davatica.server.connection.properties.allowlist=user,password,schema}). */
+  public static final String CLIENT_PROPERTIES_ALLOWLIST_KEY =
+      "avatica.server.connection.properties.allowlist";
+
   /** Special value for {@code Statement#getLargeMaxRows()} that means fetch
    * an unlimited number of rows in a single batch.
    *
@@ -100,6 +122,12 @@ public class JdbcMeta implements ProtobufMeta {
   private final Cache<Integer, StatementInfo> statementCache;
   private final MetricsSystem metrics;
 
+  /** Names of client properties always rejected; {@code null} if unset. */
+  private final Set<String> clientPropertyDenylist;
+
+  /** Names of the only client properties accepted; {@code null} if unset. */
+  private final Set<String> clientPropertyAllowlist;
+
   /**
    * Creates a JdbcMeta.
    *
@@ -209,6 +237,54 @@ public JdbcMeta(String url, Properties info, MetricsSystem 
metrics)
         return statementCache.size();
       }
     });
+
+    this.clientPropertyDenylist =
+        
parsePropertyNameList(System.getProperty(CLIENT_PROPERTIES_DENYLIST_KEY));
+    this.clientPropertyAllowlist =
+        
parsePropertyNameList(System.getProperty(CLIENT_PROPERTIES_ALLOWLIST_KEY));
+  }
+
+  /** Parses a comma-separated list of property names into a set. Returns
+   * {@code null} when {@code value} is {@code null} or blank, meaning
+   * "unconfigured". Whitespace around entries is trimmed, and blank entries
+   * are dropped. Package-private for tests. */
+  static Set<String> parsePropertyNameList(String value) {
+    if (value == null) {
+      return null;
+    }
+    final Set<String> names = new HashSet<>();
+    for (String part : value.split(",")) {
+      final String trimmed = part.trim();
+      if (!trimmed.isEmpty()) {
+        names.add(trimmed);
+      }
+    }
+    return names.isEmpty() ? null : Collections.unmodifiableSet(names);
+  }
+
+  /** Applies the configured denylist and allowlist to a client-supplied
+   * property map. Denylist is checked first; a hit rejects immediately.
+   * If a non-null allowlist is configured, any name not on it is rejected.
+   * A {@code null} list argument means the corresponding rule
+   * is unconfigured. Package-private for tests. */
+  static void checkClientProperties(Set<String> denylist,
+      Set<String> allowlist, Map<String, String> info) {
+    if (info == null || info.isEmpty()) {
+      return;
+    }
+    if (denylist == null && allowlist == null) {
+      return;
+    }
+    for (String name : info.keySet()) {
+      if (denylist != null && denylist.contains(name)) {
+        throw new ForbiddenConnectionPropertyException(name,
+            ForbiddenConnectionPropertyException.Rule.DENYLIST);
+      }
+      if (allowlist != null && !allowlist.contains(name)) {
+        throw new ForbiddenConnectionPropertyException(name,
+            ForbiddenConnectionPropertyException.Rule.ALLOWLIST);
+      }
+    }
   }
 
   // For testing purposes
@@ -610,6 +686,7 @@ public StatementHandle createStatement(ConnectionHandle ch) 
{
 
   @Override public void openConnection(ConnectionHandle ch,
       Map<String, String> info) {
+    checkClientProperties(clientPropertyDenylist, clientPropertyAllowlist, 
info);
     Properties fullInfo = new Properties();
     fullInfo.putAll(this.info);
     if (info != null) {
diff --git 
a/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java 
b/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
index 393607277..24af29b61 100644
--- a/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
@@ -32,14 +32,19 @@
 import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.fail;
 
 /**
@@ -202,6 +207,122 @@ public class JdbcMetaTest {
     // Our opened connection should get closed when this race condition happens
     Mockito.verify(conn2).close();
   }
+
+  private static Set<String> setOf(String... names) {
+    return new HashSet<>(java.util.Arrays.asList(names));
+  }
+
+  @Test public void testCheckClientPropertiesNoRulesConfigured() {
+    // With both lists null, every client name is passed through.
+    JdbcMeta.checkClientProperties(null, null,
+        Collections.singletonMap("anything", "v"));
+  }
+
+  @Test public void testCheckClientPropertiesDenylistRejectsListedName() {
+    final ForbiddenConnectionPropertyException e =
+        assertThrows(ForbiddenConnectionPropertyException.class, () ->
+            JdbcMeta.checkClientProperties(setOf("propA", "propB"), null,
+                Collections.singletonMap("propA", "v")));
+    assertThat(e.getPropertyName(), is("propA"));
+    assertThat(e.getRule(), 
is(ForbiddenConnectionPropertyException.Rule.DENYLIST));
+    assertThat(e.getMessage(), containsString("propA"));
+    assertThat(e.getMessage(), containsString("denylist"));
+    assertThat(e.getMessage(),
+        containsString(JdbcMeta.CLIENT_PROPERTIES_DENYLIST_KEY));
+  }
+
+  @Test public void testCheckClientPropertiesDenylistPermitsUnlistedName() {
+    // No exception when the name is not on the denylist.
+    JdbcMeta.checkClientProperties(setOf("propA"), null,
+        Collections.singletonMap("propOther", "v"));
+  }
+
+  @Test public void testCheckClientPropertiesAllowlistRejectsUnlistedName() {
+    final ForbiddenConnectionPropertyException e =
+        assertThrows(ForbiddenConnectionPropertyException.class, () ->
+            JdbcMeta.checkClientProperties(null, setOf("propA", "propB"),
+                Collections.singletonMap("propC", "v")));
+    assertThat(e.getPropertyName(), is("propC"));
+    assertThat(e.getRule(), 
is(ForbiddenConnectionPropertyException.Rule.ALLOWLIST));
+    assertThat(e.getMessage(), containsString("propC"));
+    assertThat(e.getMessage(), containsString("allowlist"));
+    assertThat(e.getMessage(),
+        containsString(JdbcMeta.CLIENT_PROPERTIES_ALLOWLIST_KEY));
+  }
+
+  @Test public void testCheckClientPropertiesAllowlistPermitsListedName() {
+    JdbcMeta.checkClientProperties(null, setOf("propA", "propB"),
+        Collections.singletonMap("propA", "v"));
+  }
+
+  @Test public void 
testCheckClientPropertiesDenylistAppliedBeforeAllowlistForCertainProperty() {
+    // A name on both lists is rejected by the denylist, not the allowlist,
+    // the denylist rule always wins because it is checked first.
+    final ForbiddenConnectionPropertyException e =
+        assertThrows(ForbiddenConnectionPropertyException.class, () ->
+            JdbcMeta.checkClientProperties(setOf("propA"), setOf("propA", 
"propB"),
+                Collections.singletonMap("propA", "v")));
+    assertThat(e.getRule(), 
is(ForbiddenConnectionPropertyException.Rule.DENYLIST));
+  }
+
+  @Test public void testCheckClientPropertiesEmptyMapBypassesChecks() {
+    // An empty client map trivially satisfies both lists; the check must
+    // not fabricate a rejection when there is nothing to check.
+    JdbcMeta.checkClientProperties(null, setOf("propA"), 
Collections.emptyMap());
+    JdbcMeta.checkClientProperties(setOf("propA"), setOf("propA"),
+        Collections.emptyMap());
+  }
+
+  @Test public void testCheckClientPropertiesNullMapBypassesChecks() {
+    JdbcMeta.checkClientProperties(setOf("propA"), setOf("propA"), null);
+  }
+
+  @Test public void testParsePropertyNameListNullOrBlankIsUnconfigured() {
+    // A null or all-blank value means the rule was not configured; the
+    // parser must return null so the check treats it as absent, not as an
+    // empty allowlist that would reject everything.
+    assertThat(JdbcMeta.parsePropertyNameList(null), nullValue());
+    assertThat(JdbcMeta.parsePropertyNameList(""), nullValue());
+    assertThat(JdbcMeta.parsePropertyNameList("   "), nullValue());
+    assertThat(JdbcMeta.parsePropertyNameList(" , , "), nullValue());
+  }
+
+  @Test public void testParsePropertyNameListTrimsAndDropsBlanks() {
+    // Whitespace around entries is trimmed; blank entries are dropped so
+    // that operators can format their config with padding.
+    final Set<String> parsed =
+        JdbcMeta.parsePropertyNameList(" , propA ,  ,propB ,");
+    assertThat(parsed, is(setOf("propA", "propB")));
+  }
+
+  /** End-to-end wiring check: constructing a JdbcMeta reads the JVM system
+   * property, so a rejected property short-circuits openConnection before
+   * any backend Connection is created. */
+  @Test public void testSystemPropertyRestrictsOpenConnection() throws 
SQLException {
+    final String previous =
+        System.setProperty(JdbcMeta.CLIENT_PROPERTIES_DENYLIST_KEY, "propX");
+    try {
+      final JdbcMeta meta = new JdbcMeta("jdbc:url") {
+        @Override protected Connection createConnection(String url, Properties 
info) {
+          throw new AssertionError("createConnection must not be reached "
+              + "when a client property is rejected");
+        }
+      };
+      final ForbiddenConnectionPropertyException e =
+          assertThrows(ForbiddenConnectionPropertyException.class, () ->
+              meta.openConnection(new ConnectionHandle("id-sysprop"),
+                  Collections.singletonMap("propX", "v")));
+      assertThat(e.getPropertyName(), is("propX"));
+      assertThat(e.getRule(),
+          is(ForbiddenConnectionPropertyException.Rule.DENYLIST));
+    } finally {
+      if (previous == null) {
+        System.clearProperty(JdbcMeta.CLIENT_PROPERTIES_DENYLIST_KEY);
+      } else {
+        System.setProperty(JdbcMeta.CLIENT_PROPERTIES_DENYLIST_KEY, previous);
+      }
+    }
+  }
 }
 
 // End JdbcMetaTest.java

Reply via email to