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