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

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


The following commit(s) were added to refs/heads/master by this push:
     new c23e5b54fe1 HIVE-29909: Kafka handler should reject class-loading 
client properties set as table properties (#6870)
c23e5b54fe1 is described below

commit c23e5b54fe166fccdc9e4ad452662d70aa326877
Author: Ruben Quesada Lopez <[email protected]>
AuthorDate: Tue Oct 13 16:37:38 2026 +0100

    HIVE-29909: Kafka handler should reject class-loading client properties set 
as table properties (#6870)
---
 .../hadoop/hive/kafka/KafkaStorageHandler.java     |  16 +-
 .../org/apache/hadoop/hive/kafka/KafkaUtils.java   | 175 ++++++++++-
 .../hadoop/hive/kafka/KafkaStorageHandlerTest.java |  86 ++++++
 .../apache/hadoop/hive/kafka/KafkaUtilsTest.java   | 327 +++++++++++++++++++++
 4 files changed, 595 insertions(+), 9 deletions(-)

diff --git 
a/kafka-handler/src/java/org/apache/hadoop/hive/kafka/KafkaStorageHandler.java 
b/kafka-handler/src/java/org/apache/hadoop/hive/kafka/KafkaStorageHandler.java
index 8f4b22dabd3..aa8b71ffa96 100644
--- 
a/kafka-handler/src/java/org/apache/hadoop/hive/kafka/KafkaStorageHandler.java
+++ 
b/kafka-handler/src/java/org/apache/hadoop/hive/kafka/KafkaStorageHandler.java
@@ -29,6 +29,7 @@
 import org.apache.hadoop.hive.metastore.DefaultHiveMetaHook;
 import org.apache.hadoop.hive.metastore.HiveMetaHook;
 import org.apache.hadoop.hive.metastore.TableType;
+import org.apache.hadoop.hive.metastore.api.EnvironmentContext;
 import org.apache.hadoop.hive.metastore.api.LockType;
 import org.apache.hadoop.hive.metastore.api.MetaException;
 import org.apache.hadoop.hive.metastore.api.Table;
@@ -104,6 +105,7 @@
   }
 
   private void configureCommonProperties(TableDesc tableDesc, Map<String, 
String> jobProperties) {
+    KafkaUtils.checkTableDescProperties(tableDesc.getProperties());
     String topic = 
tableDesc.getProperties().getProperty(KafkaTableProperties.HIVE_KAFKA_TOPIC.getName(),
 "");
     if (topic.isEmpty()) {
       throw new IllegalArgumentException("Kafka topic missing set table 
property->"
@@ -191,9 +193,7 @@ private void configureCommonProperties(TableDesc tableDesc, 
Map<String, String>
             .startsWith(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX))
         .forEach(entry -> {
           String key = 
entry.getKey().substring(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX.length() + 1);
-          if (KafkaUtils.FORBIDDEN_PROPERTIES.contains(key)) {
-            throw new IllegalArgumentException("Not suppose to set Kafka 
Property " + key);
-          }
+          KafkaUtils.checkTableClientProperty(key, entry.getValue());
           properties.put(key, entry.getValue());
         });
     return new KafkaStorageHandlerInfo(topic, properties);
@@ -231,9 +231,7 @@ private Properties buildProducerProperties(Table table) {
             .startsWith(KafkaUtils.PRODUCER_CONFIGURATION_PREFIX))
         .forEach(entry -> {
           String key = 
entry.getKey().substring(KafkaUtils.PRODUCER_CONFIGURATION_PREFIX.length() + 1);
-          if (KafkaUtils.FORBIDDEN_PROPERTIES.contains(key)) {
-            throw new IllegalArgumentException("Not suppose to set Kafka 
Property " + key);
-          }
+          KafkaUtils.checkTableClientProperty(key, entry.getValue());
           properties.put(key, entry.getValue());
         });
     return properties;
@@ -385,6 +383,7 @@ private String getQueryId() {
     if (!table.getTableType().equals(TableType.EXTERNAL_TABLE.toString())) {
       throw new MetaException(KAFKA_STORAGE_HANDLER + " supports only " + 
TableType.EXTERNAL_TABLE);
     }
+    KafkaUtils.checkTableProperties(table.getParameters());
     Arrays.stream(KafkaTableProperties.values())
         .filter(KafkaTableProperties::isMandatory)
         .forEach(key -> 
Preconditions.checkNotNull(table.getParameters().get(key.getName()),
@@ -401,6 +400,11 @@ private String getQueryId() {
 
   }
 
+  @Override public void preAlterTable(Table table, EnvironmentContext context) 
throws MetaException {
+    super.preAlterTable(table, context);
+    KafkaUtils.checkTableProperties(table.getParameters());
+  }
+
   @Override public void commitCreateTable(Table table) throws MetaException {
     commitInsertTable(table, false);
   }
diff --git 
a/kafka-handler/src/java/org/apache/hadoop/hive/kafka/KafkaUtils.java 
b/kafka-handler/src/java/org/apache/hadoop/hive/kafka/KafkaUtils.java
index 9e74d49a187..371e2a6416c 100644
--- a/kafka-handler/src/java/org/apache/hadoop/hive/kafka/KafkaUtils.java
+++ b/kafka-handler/src/java/org/apache/hadoop/hive/kafka/KafkaUtils.java
@@ -42,7 +42,10 @@
 import org.apache.kafka.clients.producer.ProducerConfig;
 import org.apache.kafka.clients.producer.ProducerRecord;
 import org.apache.kafka.common.config.ConfigDef;
+import org.apache.kafka.common.config.SaslConfigs;
+import org.apache.kafka.common.config.SecurityConfig;
 import org.apache.kafka.common.config.SslConfigs;
+import org.apache.kafka.common.config.types.Password;
 import org.apache.kafka.common.errors.AuthenticationException;
 import org.apache.kafka.common.errors.AuthorizationException;
 import org.apache.kafka.common.errors.InvalidTopicException;
@@ -50,6 +53,7 @@
 import org.apache.kafka.common.errors.SecurityDisabledException;
 import org.apache.kafka.common.errors.SerializationException;
 import org.apache.kafka.common.errors.UnknownServerException;
+import org.apache.kafka.common.security.JaasContext;
 import org.apache.kafka.common.security.auth.SecurityProtocol;
 import org.apache.kafka.common.serialization.ByteArrayDeserializer;
 import org.slf4j.Logger;
@@ -61,6 +65,7 @@
 import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.Base64;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
@@ -110,6 +115,172 @@ private KafkaUtils() {
           ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
           ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG));
 
+  /**
+   * System property letting an operator enable the {@link 
#CLASS_NAME_PROPERTIES} for values
+   * configured globally in hive-site.xml. It does not enable them as table 
properties.
+   */
+  static final String ALLOW_CLASS_NAME_PROPERTIES = 
"hive.kafka.client.class.name.properties.allowed";
+
+  /**
+   * Kafka client properties whose value names one or more classes that the 
client loads and
+   * instantiates in the JVM running the client. Deciding which classes the 
Hive service loads is a
+   * deployment concern, so these belong to the site configuration an operator 
controls rather than
+   * to per table DDL, and they are not accepted as table properties.
+   *
+   * <p>Most hold a single class name, while {@code interceptor.classes}, 
{@code metric.reporters}
+   * and {@code partition.assignment.strategy} hold a comma separated list. 
Kafka types the two
+   * groups differently, {@code ConfigDef.Type.CLASS} against {@code 
ConfigDef.Type.LIST}, and loads
+   * the first group while parsing the configuration and the second while 
building the client, but
+   * both end up in {@code Utils.loadClass}, so they are treated alike 
here.</p>
+   *
+   * <p>An operator who needs one of them, say a custom OAUTHBEARER callback 
handler, sets it under
+   * the {@link #CONSUMER_CONFIGURATION_PREFIX} / {@link 
#PRODUCER_CONFIGURATION_PREFIX} prefix in
+   * hive-site.xml and opts in with {@code 
-Dhive.kafka.client.class.name.properties.allowed=true}.</p>
+   */
+  static final Set<String> CLASS_NAME_PROPERTIES = 
ImmutableSet.of(SaslConfigs.SASL_LOGIN_CLASS,
+      SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS,
+      SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS,
+      SslConfigs.SSL_ENGINE_FACTORY_CLASS_CONFIG,
+      SecurityConfig.SECURITY_PROVIDERS_CONFIG,
+      CommonClientConfigs.METRIC_REPORTER_CLASSES_CONFIG,
+      ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG,
+      ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
+      ProducerConfig.PARTITIONER_CLASS_CONFIG);
+
+  /**
+   * {@code config.providers.<name>.class} names a class the same way the
+   * {@link #CLASS_NAME_PROPERTIES} do, but it carries a user chosen name in 
the middle, so it is
+   * matched by prefix rather than by set membership. The same prefix also 
covers the
+   * {@code config.providers.<name>.param.*} keys that configure such a class.
+   */
+  static final String CONFIG_PROVIDERS_PREFIX = "config.providers.";
+
+  /**
+   * Checks a property whose value is known to come from a table property, so 
the
+   * {@link #ALLOW_CLASS_NAME_PROPERTIES} opt-in does not apply.
+   *
+   * @param key the Kafka client property name, given without the Hive prefix.
+   * @param value the property value as the user supplied it.
+   * @throws IllegalArgumentException when a user must not set the property to 
that value.
+   */
+  static void checkTableClientProperty(String key, String value) {
+    checkClientProperty(key, value, true);
+  }
+
+  /**
+   * Checks a property read out of a {@link Configuration}, where a value set 
by an operator in
+   * hive-site.xml is indistinguishable from a table property, so the
+   * {@link #ALLOW_CLASS_NAME_PROPERTIES} opt-in applies.
+   *
+   * @param key the Kafka client property name, given without the Hive prefix.
+   * @param value the property value, which Hive has escaped if it came from a 
table property.
+   * @throws IllegalArgumentException when a user must not set the property to 
that value.
+   */
+  static void checkConfiguredClientProperty(String key, String value) {
+    checkClientProperty(key, value, false);
+  }
+
+  /**
+   * The single policy for one Kafka client property, name given without the 
Hive prefix. There is
+   * one policy and it is applied everywhere; the only thing that varies 
between call sites is
+   * whether the value's provenance is known, which is what {@code 
provenanceIsTable} records.
+   *
+   * @param key the Kafka client property name, e.g. {@code sasl.login.class}.
+   * @param value the property value.
+   * @param provenanceIsTable true when the value is known to come from a 
table property. False
+   *     only for a {@link Configuration}, in which Hive has already merged 
hive-site.xml with the table
+   *     properties ({@code Utilities.copyTableJobPropertiesToConf}) so the 
two can no longer be told apart.
+   *     Since an operator legitimately needs the {@link 
#CLASS_NAME_PROPERTIES} from hive-site.xml,
+   *     that ambiguous case is the only one where {@link 
#ALLOW_CLASS_NAME_PROPERTIES} can re-enable them.
+   * @throws IllegalArgumentException when a user must not set the property to 
that value.
+   */
+  private static void checkClientProperty(String key, String value, boolean 
provenanceIsTable) {
+    if (FORBIDDEN_PROPERTIES.contains(key)) {
+      throw new IllegalArgumentException("Not suppose to set Kafka Property " 
+ key);
+    }
+    if (CLASS_NAME_PROPERTIES.contains(key) || 
key.startsWith(CONFIG_PROVIDERS_PREFIX)) {
+      if (provenanceIsTable) {
+        throw new IllegalArgumentException("Kafka Property " + key + " 
configures a class that the Hive service "
+            + "would load and run, therefore it can not be set as a table 
property.");
+      }
+      if (!Boolean.getBoolean(ALLOW_CLASS_NAME_PROPERTIES)) {
+        throw new IllegalArgumentException("Kafka Property " + key + " 
configures a class that the Hive service "
+            + "would load and run, therefore it can not be set here. An 
operator can set -D"
+            + ALLOW_CLASS_NAME_PROPERTIES + "=true to allow values configured 
in hive-site.xml.");
+      }
+    }
+    if (SaslConfigs.SASL_JAAS_CONFIG.equals(key)) {
+      checkJaasConfig(value);
+    }
+  }
+
+  /**
+   * Rejects a {@link SaslConfigs#SASL_JAAS_CONFIG} value Kafka would not 
accept, reusing Kafka's own
+   * mechanisms. Kafka applies the same check when the client is built; 
running it here fails the DDL
+   * rather than the query.
+   *
+   * @param jaasConfig the user supplied {@code sasl.jaas.config} value.
+   * @throws IllegalArgumentException when the value does not parse or names a 
refused login module.
+   */
+  static void checkJaasConfig(String jaasConfig) {
+    try {
+      
JaasContext.loadClientContext(Collections.singletonMap(SaslConfigs.SASL_JAAS_CONFIG,
 new Password(jaasConfig)));
+    } catch (RuntimeException e) {
+      throw new IllegalArgumentException("Refused value for Kafka Property " + 
SaslConfigs.SASL_JAAS_CONFIG + ": "
+          + e.getMessage(), e);
+    }
+  }
+
+  /**
+   * Checks the Kafka client properties a user supplied as table properties, 
before the table is
+   * created or altered.
+   *
+   * @param tableProperties the table parameters as supplied by the user, may 
be null.
+   * @throws IllegalArgumentException when a user must not set one of the 
table properties.
+   */
+  static void checkTableProperties(Map<String, String> tableProperties) {
+    if (tableProperties == null) {
+      return;
+    }
+    for (Map.Entry<String, String> entry : tableProperties.entrySet()) {
+      checkTableProperty(entry.getKey(), entry.getValue());
+    }
+  }
+
+  /**
+   * Same as {@link #checkTableProperties(Map)} for the {@link Properties} 
view of a table.
+   *
+   * @param tableProperties the table properties, may be null.
+   * @throws IllegalArgumentException when a user must not set one of the 
table properties.
+   */
+  static void checkTableDescProperties(Properties tableProperties) {
+    if (tableProperties == null) {
+      return;
+    }
+    for (String name : tableProperties.stringPropertyNames()) {
+      checkTableProperty(name, tableProperties.getProperty(name));
+    }
+  }
+
+  /**
+   * Checks one table property, whose name might carry the {@value 
#CONSUMER_CONFIGURATION_PREFIX}
+   * or {@value #PRODUCER_CONFIGURATION_PREFIX} prefix.
+   */
+  private static void checkTableProperty(String rawKey, String value) {
+    String key = rawKey;
+    for (String prefix : new String[] {CONSUMER_CONFIGURATION_PREFIX + ".", 
PRODUCER_CONFIGURATION_PREFIX + "."}) {
+      if (key.startsWith(prefix)) {
+        key = key.substring(prefix.length());
+        break;
+      }
+    }
+    try {
+      checkTableClientProperty(key, value);
+    } catch (IllegalArgumentException e) {
+      throw new IllegalArgumentException("Refused table property " + rawKey + 
": " + e.getMessage(), e);
+    }
+  }
+
   /**
    * @param configuration Job configs
    *
@@ -231,9 +402,7 @@ private static Map<String, String> 
extractExtraProperties(final Configuration co
     final Map<String, String> kafkaProperties = 
configuration.getValByRegex("^" + prefix + "\\..*");
     for (Map.Entry<String, String> entry : kafkaProperties.entrySet()) {
       String key = entry.getKey().substring(prefix.length() + 1);
-      if (FORBIDDEN_PROPERTIES.contains(key)) {
-        throw new IllegalArgumentException("Not suppose to set Kafka Property 
" + key);
-      }
+      checkConfiguredClientProperty(key, entry.getValue());
       builder.put(key, entry.getValue());
     }
     return builder.build();
diff --git 
a/kafka-handler/src/test/org/apache/hadoop/hive/kafka/KafkaStorageHandlerTest.java
 
b/kafka-handler/src/test/org/apache/hadoop/hive/kafka/KafkaStorageHandlerTest.java
index 7d6f889f97d..bef51ade03a 100644
--- 
a/kafka-handler/src/test/org/apache/hadoop/hive/kafka/KafkaStorageHandlerTest.java
+++ 
b/kafka-handler/src/test/org/apache/hadoop/hive/kafka/KafkaStorageHandlerTest.java
@@ -29,6 +29,8 @@
 import org.apache.kafka.clients.CommonClientConfigs;
 import org.apache.kafka.clients.consumer.ConsumerConfig;
 import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.config.SaslConfigs;
+import org.apache.kafka.common.config.SslConfigs;
 import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -143,4 +145,88 @@ public class KafkaStorageHandlerTest {
     Assert.assertEquals(String.valueOf(Integer.MAX_VALUE), 
producerProperties.get(ProducerConfig.RETRIES_CONFIG));
     Assert.assertEquals("true", 
producerProperties.get(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG));
   }
+
+  private static Table kafkaTable(String key, String value) {
+    Table table = new Table();
+    table.setTableType(TableType.EXTERNAL_TABLE.toString());
+    table.putToParameters(KafkaTableProperties.HIVE_KAFKA_TOPIC.getName(), 
TEST_TOPIC);
+    
table.putToParameters(KafkaTableProperties.HIVE_KAFKA_BOOTSTRAP_SERVERS.getName(),
 LOCALHOST_9291);
+    table.putToParameters(key, value);
+    return table;
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
preCreateTableRefusesDisallowedLoginModule()
+      throws MetaException {
+    new KafkaStorageHandler().preCreateTable(kafkaTable(
+        KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." + 
SaslConfigs.SASL_JAAS_CONFIG,
+        "com.sun.security.auth.module.JndiLoginModule required 
user.provider.url=\"ldap://ldap.example.com:389/ou=users\";";));
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
preCreateTableRefusesCallbackHandlerClass()
+      throws MetaException {
+    new KafkaStorageHandler().preCreateTable(kafkaTable(
+        KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." + 
SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS,
+        "com.example.CustomCallbackHandler"));
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
preCreateTableRefusesUnprefixedEngineFactory()
+      throws MetaException {
+    new KafkaStorageHandler().preCreateTable(
+        kafkaTable(SslConfigs.SSL_ENGINE_FACTORY_CLASS_CONFIG, 
"com.example.CustomEngineFactory"));
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
preAlterTableRefusesCallbackHandlerClass()
+      throws MetaException {
+    new KafkaStorageHandler().preAlterTable(kafkaTable(
+        KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." + 
SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS,
+        "com.example.CustomCallbackHandler"), null);
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
preCreateTableRefusesProducerSideEngineFactory()
+      throws MetaException {
+    new KafkaStorageHandler().preCreateTable(kafkaTable(
+        KafkaUtils.PRODUCER_CONFIGURATION_PREFIX + "." + 
SslConfigs.SSL_ENGINE_FACTORY_CLASS_CONFIG,
+        "com.example.CustomEngineFactory"));
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
preCreateTableRefusesConfigProviderClass()
+      throws MetaException {
+    new KafkaStorageHandler().preCreateTable(kafkaTable(
+        KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." + 
KafkaUtils.CONFIG_PROVIDERS_PREFIX + "custom.class",
+        "com.example.CustomConfigProvider"));
+  }
+
+  @Test public void preCreateTableAcceptsOrdinaryConsumerProperty() throws 
MetaException {
+    Table table = kafkaTable(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + 
".max.poll.records", "5000");
+    new KafkaStorageHandler().preCreateTable(table);
+    Assert.assertEquals("5000",
+        table.getParameters().get(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + 
".max.poll.records"));
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
storageHandlerInfoRefusesCallbackHandlerClass()
+      throws MetaException {
+    KafkaStorageHandler kafkaStorageHandler = new KafkaStorageHandler();
+    kafkaStorageHandler.setConf(new JobConf());
+    kafkaStorageHandler.getStorageHandlerInfo(kafkaTable(
+        KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." + 
SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS,
+        "com.example.CustomCallbackHandler"));
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
storageHandlerInfoRefusesConfigProviderClass()
+      throws MetaException {
+    KafkaStorageHandler kafkaStorageHandler = new KafkaStorageHandler();
+    kafkaStorageHandler.setConf(new JobConf());
+    kafkaStorageHandler.getStorageHandlerInfo(kafkaTable(
+        KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." + 
KafkaUtils.CONFIG_PROVIDERS_PREFIX + "custom.class",
+        "com.example.CustomConfigProvider"));
+  }
+
+  @Test(expected = IllegalArgumentException.class) public void 
storageHandlerInfoRefusesDisallowedLoginModule()
+      throws MetaException {
+    KafkaStorageHandler kafkaStorageHandler = new KafkaStorageHandler();
+    kafkaStorageHandler.setConf(new JobConf());
+    kafkaStorageHandler.getStorageHandlerInfo(kafkaTable(
+        KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." + 
SaslConfigs.SASL_JAAS_CONFIG,
+        "com.sun.security.auth.module.JndiLoginModule required 
user.provider.url=\"ldap://ldap.example.com:389/ou=users\";";));
+  }
 }
diff --git 
a/kafka-handler/src/test/org/apache/hadoop/hive/kafka/KafkaUtilsTest.java 
b/kafka-handler/src/test/org/apache/hadoop/hive/kafka/KafkaUtilsTest.java
index b8c43b96a47..7ba35777c51 100644
--- a/kafka-handler/src/test/org/apache/hadoop/hive/kafka/KafkaUtilsTest.java
+++ b/kafka-handler/src/test/org/apache/hadoop/hive/kafka/KafkaUtilsTest.java
@@ -25,13 +25,24 @@
 import org.apache.hadoop.io.LongWritable;
 import org.apache.hadoop.io.Writable;
 import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.config.ConfigDef;
+import org.apache.kafka.common.config.SaslConfigs;
+import org.apache.kafka.common.config.SslConfigs;
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Test;
 
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Test for Utility class.
@@ -65,6 +76,322 @@ public KafkaUtilsTest() {
     KafkaUtils.consumerProperties(configuration);
   }
 
+  @After public void clearClassNamePropertyOptIn() {
+    System.clearProperty(KafkaUtils.ALLOW_CLASS_NAME_PROPERTIES);
+  }
+
+  private static Configuration consumerConfig(String key, String value) {
+    Configuration configuration = new Configuration();
+    configuration.set("kafka.bootstrap.servers", "localhost:9090");
+    configuration.set(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." + key, 
value);
+    return configuration;
+  }
+
+  private static final String CUSTOM_CLASS = "com.example.CustomClass";
+  private static final String CUSTOM_CONFIG_PROVIDER = 
"com.example.CustomConfigProvider";
+
+  @Test public void canNotSetClassNamingPropertiesOnConsumer() {
+    for (String key : KafkaUtils.CLASS_NAME_PROPERTIES) {
+      try {
+        KafkaUtils.consumerProperties(consumerConfig(key, CUSTOM_CLASS));
+        Assert.fail("Expected " + key + " to be refused");
+      } catch (IllegalArgumentException e) {
+        Assert.assertTrue("Unexpected message: " + e.getMessage(), 
e.getMessage().contains(key));
+      }
+    }
+  }
+
+  @Test public void canNotSetClassNamingPropertiesOnProducer() {
+    for (String key : KafkaUtils.CLASS_NAME_PROPERTIES) {
+      Configuration configuration = new Configuration();
+      configuration.set("kafka.bootstrap.servers", "localhost:9090");
+      configuration.set(KafkaTableProperties.WRITE_SEMANTIC_PROPERTY.getName(),
+          KafkaOutputFormat.WriteSemantic.AT_LEAST_ONCE.name());
+      configuration.set(KafkaUtils.PRODUCER_CONFIGURATION_PREFIX + "." + key, 
CUSTOM_CLASS);
+      try {
+        KafkaUtils.producerProperties(configuration);
+        Assert.fail("Expected " + key + " to be refused");
+      } catch (IllegalArgumentException e) {
+        Assert.assertTrue("Unexpected message: " + e.getMessage(), 
e.getMessage().contains(key));
+      }
+    }
+  }
+
+  @Test public void operatorCanOptInToClassNamingPropertiesGlobally() {
+    System.setProperty(KafkaUtils.ALLOW_CLASS_NAME_PROPERTIES, "true");
+    Properties properties =
+        
KafkaUtils.consumerProperties(consumerConfig(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS,
 "com.acme.OAuth"));
+    Assert.assertEquals("com.acme.OAuth", 
properties.getProperty(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+  }
+
+  @Test public void optInDoesNotReachTableProperties() {
+    System.setProperty(KafkaUtils.ALLOW_CLASS_NAME_PROPERTIES, "true");
+    assertTablePropertyRefused(SslConfigs.SSL_ENGINE_FACTORY_CLASS_CONFIG, 
"com.example.CustomEngineFactory");
+  }
+
+  private static void assertTablePropertyRefused(String key, String value) {
+    assertTablePropertyRefused(key, value, key);
+  }
+
+  private static void assertTablePropertyRefused(String key, String value, 
String expectedInMessage) {
+    try {
+      KafkaUtils.checkTableProperties(Collections.singletonMap(key, value));
+      Assert.fail("Expected table property " + key + " to be refused");
+    } catch (IllegalArgumentException e) {
+      Assert.assertTrue("Unexpected message: " + e.getMessage(), 
e.getMessage().contains(expectedInMessage));
+    }
+  }
+
+  /**
+   * The un-prefixed spelling reaches the Kafka client through 
copySSLProperties, so it has to be
+   * refused at DDL time, where we still know the value came from a table 
property.
+   */
+  @Test public void unprefixedClassNamingTablePropertyIsRefused() {
+    for (String key : KafkaUtils.CLASS_NAME_PROPERTIES) {
+      assertTablePropertyRefused(key, CUSTOM_CLASS);
+    }
+  }
+
+  @Test public void prefixedClassNamingTablePropertiesAreRefused() {
+    for (String prefix : new String[] 
{KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + ".",
+        KafkaUtils.PRODUCER_CONFIGURATION_PREFIX + "."}) {
+      for (String key : KafkaUtils.CLASS_NAME_PROPERTIES) {
+        assertTablePropertyRefused(prefix + key, CUSTOM_CLASS);
+      }
+    }
+  }
+
+  /**
+   * The config provider keys carry a user chosen name in the middle, so they 
are matched by prefix.
+   */
+  @Test public void configProviderClassTablePropertyIsRefused() {
+    assertTablePropertyRefused(KafkaUtils.CONFIG_PROVIDERS_PREFIX + 
"custom.class", CUSTOM_CONFIG_PROVIDER);
+  }
+
+  @Test public void prefixedConfigProviderClassTablePropertyIsRefused() {
+    assertTablePropertyRefused(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "."
+            + KafkaUtils.CONFIG_PROVIDERS_PREFIX + "custom.class", 
CUSTOM_CONFIG_PROVIDER,
+        KafkaUtils.CONFIG_PROVIDERS_PREFIX + "custom.class");
+  }
+
+  /**
+   * The parameters handed to such a class share the prefix, so they are 
refused with it. On their
+   * own they are inert, Kafka only reads them once a provider class is 
configured.
+   */
+  @Test public void configProviderParameterTablePropertyIsRefused() {
+    assertTablePropertyRefused(KafkaUtils.CONFIG_PROVIDERS_PREFIX + 
"custom.param.allowed.paths", "/etc");
+  }
+
+  @Test public void configProviderClassIsRefusedOnConfigurationPath() {
+    try {
+      
KafkaUtils.consumerProperties(consumerConfig(KafkaUtils.CONFIG_PROVIDERS_PREFIX 
+ "custom.class",
+          CUSTOM_CONFIG_PROVIDER));
+      Assert.fail("Expected the config provider class to be refused");
+    } catch (IllegalArgumentException e) {
+      Assert.assertTrue("Unexpected message: " + e.getMessage(),
+          e.getMessage().contains(KafkaUtils.CONFIG_PROVIDERS_PREFIX + 
"custom.class"));
+    }
+  }
+
+  @Test public void operatorCanOptInToConfigProvidersGlobally() {
+    System.setProperty(KafkaUtils.ALLOW_CLASS_NAME_PROPERTIES, "true");
+    Properties properties = KafkaUtils.consumerProperties(
+        consumerConfig(KafkaUtils.CONFIG_PROVIDERS_PREFIX + "custom.class", 
CUSTOM_CONFIG_PROVIDER));
+    Assert.assertEquals(CUSTOM_CONFIG_PROVIDER,
+        properties.getProperty(KafkaUtils.CONFIG_PROVIDERS_PREFIX + 
"custom.class"));
+  }
+
+  @Test public void optInDoesNotReachConfigProvidersOnTableClientPath() {
+    System.setProperty(KafkaUtils.ALLOW_CLASS_NAME_PROPERTIES, "true");
+    try {
+      KafkaUtils.checkTableClientProperty(KafkaUtils.CONFIG_PROVIDERS_PREFIX + 
"custom.class",
+          CUSTOM_CONFIG_PROVIDER);
+      Assert.fail("Expected the config provider class to be refused");
+    } catch (IllegalArgumentException e) {
+      Assert.assertTrue("Unexpected message: " + e.getMessage(),
+          e.getMessage().contains(KafkaUtils.CONFIG_PROVIDERS_PREFIX + 
"custom.class"));
+    }
+  }
+
+  /**
+   * Kafka marks the properties it resolves into a class with {@link 
ConfigDef.Type#CLASS}. Pinning
+   * that set against the two lists in KafkaUtils means a Kafka upgrade that 
adds one of them fails
+   * here, rather than quietly arriving as a table property nothing looks at. 
It cannot cover the
+   * handful Kafka types as a list or a plain string, those stay enumerated by 
hand.
+   */
+  @Test public void everyClassTypedKafkaPropertyIsListed() {
+    Set<String> classTyped = Stream.of(ConsumerConfig.configDef(), 
ProducerConfig.configDef())
+        .flatMap(configDef -> configDef.configKeys().entrySet().stream())
+        .filter(entry -> entry.getValue().type == ConfigDef.Type.CLASS)
+        .map(Map.Entry::getKey)
+        .collect(Collectors.toSet());
+    // Guard against the lookup itself silently returning nothing, which would 
leave the assertion
+    // below passing for the wrong reason.
+    Assert.assertTrue("Expected to find the class typed Kafka properties, 
found " + classTyped,
+        classTyped.contains(SslConfigs.SSL_ENGINE_FACTORY_CLASS_CONFIG)
+            && classTyped.contains(ProducerConfig.PARTITIONER_CLASS_CONFIG));
+
+    Set<String> listed = new HashSet<>(KafkaUtils.CLASS_NAME_PROPERTIES);
+    listed.addAll(KafkaUtils.FORBIDDEN_PROPERTIES);
+    List<String> unlisted = classTyped.stream().filter(name -> 
!listed.contains(name)).sorted()
+        .collect(Collectors.toList());
+    Assert.assertEquals("Kafka properties resolved into a class that neither 
list in KafkaUtils covers",
+        Collections.emptyList(), unlisted);
+  }
+
+  @Test public void ordinaryTablePropertiesAreAccepted() {
+    Map<String, String> tableProperties = new HashMap<>();
+    tableProperties.put(KafkaTableProperties.HIVE_KAFKA_TOPIC.getName(), 
"a-topic");
+    tableProperties.put(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + 
".max.poll.records", "5000");
+    tableProperties.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, 
"/etc/ssl/truststore.jks");
+    KafkaUtils.checkTableProperties(tableProperties);
+  }
+
+  @Test public void nullTablePropertiesAreAccepted() {
+    KafkaUtils.checkTableProperties(null);
+  }
+
+  @Test public void forbiddenPropertyIsRefusedOnTableClientPath() {
+    for (String key : KafkaUtils.FORBIDDEN_PROPERTIES) {
+      try {
+        KafkaUtils.checkTableClientProperty(key, "whatever");
+        Assert.fail("Expected " + key + " to be refused");
+      } catch (IllegalArgumentException e) {
+        Assert.assertTrue("Unexpected message: " + e.getMessage(), 
e.getMessage().contains(key));
+      }
+    }
+  }
+
+  /**
+   * The operator opt-in covers values an operator put in hive-site.xml. A 
value read back out of the
+   * table parameters is user supplied DDL whatever the opt-in says, so it 
must stay refused.
+   */
+  @Test public void optInDoesNotReachTableClientPath() {
+    System.setProperty(KafkaUtils.ALLOW_CLASS_NAME_PROPERTIES, "true");
+    for (String key : KafkaUtils.CLASS_NAME_PROPERTIES) {
+      try {
+        KafkaUtils.checkTableClientProperty(key, CUSTOM_CLASS);
+        Assert.fail("Expected " + key + " to be refused");
+      } catch (IllegalArgumentException e) {
+        Assert.assertTrue("Unexpected message: " + e.getMessage(), 
e.getMessage().contains(key));
+      }
+    }
+  }
+
+  @Test public void disallowedLoginModuleIsRefusedOnTableClientPath() {
+    try {
+      KafkaUtils.checkTableClientProperty(SaslConfigs.SASL_JAAS_CONFIG,
+          "com.sun.security.auth.module.JndiLoginModule required 
user.provider.url=\"ldap://ldap.example.com:389/ou=users\";";);
+      Assert.fail("Expected the disallowed login module to be refused");
+    } catch (IllegalArgumentException e) {
+      Assert.assertTrue("Unexpected message: " + e.getMessage(), 
e.getMessage().contains("JndiLoginModule"));
+    }
+  }
+
+  @Test public void ordinaryPropertyIsAcceptedOnTableClientPath() {
+    KafkaUtils.checkTableClientProperty("max.poll.records", "5000");
+  }
+
+  /**
+   * The functional guard rails now fail the DDL rather than only the query, 
since checkTableProperties
+   * and the paths reading the parameters back share one gate.
+   */
+  @Test public void forbiddenTablePropertyIsRefusedAtDdl() {
+    assertTablePropertyRefused(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "."
+        + ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true", 
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG);
+  }
+
+  /**
+   * A login module Kafka itself refuses through {@code 
org.apache.kafka.disallowed.login.modules} is
+   * reported at DDL time instead of at query time. The prefixed key also 
covers the prefix
+   * stripping, which is why the message names the un-prefixed Kafka property.
+   */
+  @Test public void disallowedLoginModuleIsRefusedInJaasConfig() {
+    assertTablePropertyRefused(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "." 
+ SaslConfigs.SASL_JAAS_CONFIG,
+        "com.sun.security.auth.module.JndiLoginModule required 
user.provider.url=\"ldap://ldap.example.com:389/ou=users\";";,
+        "JndiLoginModule");
+  }
+
+  @Test public void secondDisallowedLoginModuleIsRefusedInJaasConfig() {
+    assertTablePropertyRefused(SaslConfigs.SASL_JAAS_CONFIG,
+        "com.sun.security.auth.module.LdapLoginModule required 
userProvider=\"ldap://ldap.example.com:389/o=example\";";,
+        "LdapLoginModule");
+  }
+
+  @Test public void unparseableJaasConfigIsRefused() {
+    assertTablePropertyRefused(SaslConfigs.SASL_JAAS_CONFIG, "not a jaas 
config at all");
+  }
+
+  @Test public void standardLoginModuleIsAcceptedInJaasConfig() {
+    
KafkaUtils.checkJaasConfig("org.apache.kafka.common.security.plain.PlainLoginModule
 required "
+        + "username=\"user\" password=\"pass\";");
+  }
+
+  /**
+   * A semicolon inside a quoted option value must not be mistaken for an 
entry separator, which is
+   * why Kafka's own parser is used rather than splitting the value by hand.
+   */
+  @Test public void semicolonInsideQuotedValueIsAcceptedInJaasConfig() {
+    
KafkaUtils.checkJaasConfig("org.apache.kafka.common.security.scram.ScramLoginModule
 required "
+        + "username=\"user\" password=\"pa;ss\";");
+  }
+
+  /**
+   * Third party login modules such as the AWS MSK IAM one stay usable: the 
policy is Kafka's deny
+   * list, not an allow list of the four modules Hive happens to know about.
+   */
+  @Test public void thirdPartyLoginModuleIsAcceptedInJaasConfig() {
+    KafkaUtils.checkJaasConfig("software.amazon.msk.auth.iam.IAMLoginModule 
required;");
+  }
+
+  /**
+   * A value an operator put in hive-site.xml arrives literal and has to keep 
working, which is the
+   * whole reason the Configuration path is more permissive than the table 
property path.
+   */
+  @Test public void configuredJaasValueFromSiteConfigIsAccepted() {
+    Properties properties = 
KafkaUtils.consumerProperties(consumerConfig(SaslConfigs.SASL_JAAS_CONFIG,
+        "org.apache.kafka.common.security.plain.PlainLoginModule required 
username=\"user\" password=\"pass\";"));
+    Assert.assertNotNull(properties.getProperty(SaslConfigs.SASL_JAAS_CONFIG));
+  }
+
+  /**
+   * The value is checked on the Configuration path too. A table property 
could only arrive here
+   * escapeJava'd by Utilities.copyTableJobPropertiesToConf, which Kafka's own 
JAAS parser rejects
+   * anyway, so there is no working table this leniency would have protected.
+   */
+  @Test public void configuredJaasValueWithDisallowedLoginModuleIsRefused() {
+    try {
+      
KafkaUtils.consumerProperties(consumerConfig(SaslConfigs.SASL_JAAS_CONFIG,
+          "com.sun.security.auth.module.JndiLoginModule required 
user.provider.url=\"ldap://ldap.example.com:389/ou=users\";";));
+      Assert.fail("Expected the disallowed login module to be refused");
+    } catch (IllegalArgumentException e) {
+      Assert.assertTrue("Unexpected message: " + e.getMessage(), 
e.getMessage().contains("JndiLoginModule"));
+    }
+  }
+
+  /**
+   * The compile time check runs on the table sourced TableDesc properties, 
before Hive merges them
+   * into the job configuration, so a table property never becomes 
indistinguishable from operator
+   * configuration. Without it the operator opt-in would also re-enable table 
properties.
+   */
+  @Test public void tablePropertiesAreRefusedEvenWhenOperatorOptedIn() {
+    System.setProperty(KafkaUtils.ALLOW_CLASS_NAME_PROPERTIES, "true");
+    Properties tableProperties = new Properties();
+    tableProperties.setProperty(KafkaUtils.CONSUMER_CONFIGURATION_PREFIX + "."
+        + SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS, 
"com.example.CustomCallbackHandler");
+    try {
+      KafkaUtils.checkTableDescProperties(tableProperties);
+      Assert.fail("Expected the table property to be refused");
+    } catch (IllegalArgumentException e) {
+      Assert.assertTrue("Unexpected message: " + e.getMessage(),
+          
e.getMessage().contains(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS));
+    }
+  }
+
+  @Test public void nullPropertiesViewIsAccepted() {
+    KafkaUtils.checkTableDescProperties(null);
+  }
+
   @Test public void testSetupKafkaSslPropertiesNoSslIsUnchanged() {
     Configuration config = new Configuration();
     Properties props = new Properties();

Reply via email to