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

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


The following commit(s) were added to refs/heads/master by this push:
     new b706c68cd [AMORO-1258] Support Zookeeper kerberos authentication 
(#3980)
b706c68cd is described below

commit b706c68cdc85341a18d48654d80b7a97946f5370
Author: Fei Wang <[email protected]>
AuthorDate: Mon Dec 8 01:27:53 2025 -0800

    [AMORO-1258] Support Zookeeper kerberos authentication (#3980)
    
    * [AMORO-1258] Support Zookeeper kerberos authentication
    
    * remove minkdc
    
    * Revert "remove minkdc"
    
    This reverts commit 466d6320dfee64f8324be1ed35b8cc1c39a05b7f.
    
    * docs
---
 amoro-ams/pom.xml                                  |  11 ++
 .../apache/amoro/server/AmoroManagementConf.java   |  19 +++
 .../amoro/server/HighAvailabilityContainer.java    |  59 ++++++++
 .../server/HighAvailabilityContainerTest.java      |  76 ++++++++++
 .../amoro/server/util/KerberizedTestHelper.java    | 166 +++++++++++++++++++++
 docs/configuration/ams-config.md                   |   3 +
 pom.xml                                            |  34 +++++
 7 files changed, 368 insertions(+)

diff --git a/amoro-ams/pom.xml b/amoro-ams/pom.xml
index cee916f3d..14ba45c6f 100644
--- a/amoro-ams/pom.xml
+++ b/amoro-ams/pom.xml
@@ -472,6 +472,17 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.hadoop</groupId>
+            <artifactId>hadoop-minikdc</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.directory.jdbm</groupId>
+            <artifactId>apacheds-jdbm1</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
index 77e92ccff..8ba5439bd 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
@@ -216,6 +216,25 @@ public class AmoroManagementConf {
           .defaultValue("")
           .withDescription("The Zookeeper address used for high 
availability.");
 
+  public static final ConfigOption<String> HA_ZOOKEEPER_AUTH_TYPE =
+      ConfigOptions.key("ha.zookeeper-auth-type")
+          .stringType()
+          .defaultValue("NONE")
+          .withDescription("The Zookeeper authentication type, NONE or 
KERBEROS.");
+
+  public static final ConfigOption<String> HA_ZOOKEEPER_AUTH_KEYTAB =
+      ConfigOptions.key("ha.zookeeper-auth-keytab")
+          .stringType()
+          .defaultValue("")
+          .withDescription(
+              "The Zookeeper authentication keytab file path when auth type is 
KERBEROS.");
+
+  public static final ConfigOption<String> HA_ZOOKEEPER_AUTH_PRINCIPAL =
+      ConfigOptions.key("ha.zookeeper-auth-principal")
+          .stringType()
+          .defaultValue("")
+          .withDescription("The Zookeeper authentication principal when auth 
type is KERBEROS.");
+
   public static final ConfigOption<Duration> HA_ZOOKEEPER_SESSION_TIMEOUT =
       ConfigOptions.key("ha.session-timeout")
           .durationType()
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/HighAvailabilityContainer.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/HighAvailabilityContainer.java
index 6d15d3735..7f74d3924 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/HighAvailabilityContainer.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/HighAvailabilityContainer.java
@@ -21,6 +21,8 @@ package org.apache.amoro.server;
 import org.apache.amoro.client.AmsServerInfo;
 import org.apache.amoro.config.Configurations;
 import org.apache.amoro.properties.AmsHAProperties;
+import org.apache.amoro.shade.guava32.com.google.common.base.Preconditions;
+import org.apache.amoro.shade.guava32.com.google.common.collect.Maps;
 import 
org.apache.amoro.shade.zookeeper3.org.apache.curator.framework.CuratorFramework;
 import 
org.apache.amoro.shade.zookeeper3.org.apache.curator.framework.CuratorFrameworkFactory;
 import 
org.apache.amoro.shade.zookeeper3.org.apache.curator.framework.api.transaction.CuratorOp;
@@ -29,12 +31,21 @@ import 
org.apache.amoro.shade.zookeeper3.org.apache.curator.framework.recipes.le
 import 
org.apache.amoro.shade.zookeeper3.org.apache.curator.retry.ExponentialBackoffRetry;
 import org.apache.amoro.shade.zookeeper3.org.apache.zookeeper.CreateMode;
 import org.apache.amoro.shade.zookeeper3.org.apache.zookeeper.KeeperException;
+import org.apache.amoro.utils.DynConstructors;
 import org.apache.amoro.utils.JacksonUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.hadoop.security.SecurityUtil;
+import org.apache.hadoop.security.UserGroupInformation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.security.auth.login.Configuration;
+
+import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 
 public class HighAvailabilityContainer implements LeaderLatchListener {
@@ -60,6 +71,7 @@ public class HighAvailabilityContainer implements 
LeaderLatchListener {
       tableServiceMasterPath = 
AmsHAProperties.getTableServiceMasterPath(haClusterName);
       optimizingServiceMasterPath = 
AmsHAProperties.getOptimizingServiceMasterPath(haClusterName);
       ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 
3, 5000);
+      setupZookeeperAuth(serviceConfig);
       this.zkClient =
           CuratorFrameworkFactory.builder()
               .connectString(zkServerAddress)
@@ -178,4 +190,51 @@ public class HighAvailabilityContainer implements 
LeaderLatchListener {
       // ignore
     }
   }
+
+  private static final Map<Pair<String, String>, Configuration> 
JAAS_CONFIGURATION_CACHE =
+      Maps.newConcurrentMap();
+
+  /** For a kerberized cluster, we dynamically set up the client's JAAS conf. 
*/
+  public static void setupZookeeperAuth(Configurations configurations) throws 
IOException {
+    String zkAuthType = 
configurations.get(AmoroManagementConf.HA_ZOOKEEPER_AUTH_TYPE);
+    if ("KERBEROS".equalsIgnoreCase(zkAuthType) && 
UserGroupInformation.isSecurityEnabled()) {
+      String principal = 
configurations.get(AmoroManagementConf.HA_ZOOKEEPER_AUTH_PRINCIPAL);
+      String keytab = 
configurations.get(AmoroManagementConf.HA_ZOOKEEPER_AUTH_KEYTAB);
+      Preconditions.checkArgument(
+          StringUtils.isNoneBlank(principal, keytab),
+          "%s and %s must be provided for KERBEROS authentication",
+          AmoroManagementConf.HA_ZOOKEEPER_AUTH_PRINCIPAL.key(),
+          AmoroManagementConf.HA_ZOOKEEPER_AUTH_KEYTAB.key());
+      if (!new File(keytab).exists()) {
+        throw new IOException(
+            String.format(
+                "%s: %s does not exist",
+                AmoroManagementConf.HA_ZOOKEEPER_AUTH_KEYTAB.key(), keytab));
+      }
+      System.setProperty("zookeeper.sasl.clientconfig", 
"AmoroZooKeeperClient");
+      String zkClientPrincipal = SecurityUtil.getServerPrincipal(principal, 
"0.0.0.0");
+      Configuration jaasConf =
+          JAAS_CONFIGURATION_CACHE.computeIfAbsent(
+              Pair.of(principal, keytab),
+              pair -> {
+                // HDFS-16591 makes breaking change on JaasConfiguration
+                return DynConstructors.builder()
+                    .impl( // Hadoop 3.3.5 and above
+                        
"org.apache.hadoop.security.authentication.util.JaasConfiguration",
+                        String.class,
+                        String.class,
+                        String.class)
+                    .impl( // Hadoop 3.3.4 and previous
+                        // scalastyle:off
+                        
"org.apache.hadoop.security.token.delegation.ZKDelegationTokenSecretManager$JaasConfiguration",
+                        // scalastyle:on
+                        String.class,
+                        String.class,
+                        String.class)
+                    .<Configuration>build()
+                    .newInstance("AmoroZooKeeperClient", zkClientPrincipal, 
keytab);
+              });
+      Configuration.setConfiguration(jaasConf);
+    }
+  }
 }
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/HighAvailabilityContainerTest.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/HighAvailabilityContainerTest.java
new file mode 100644
index 000000000..d29a1a9ac
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/HighAvailabilityContainerTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.amoro.server;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.amoro.config.Configurations;
+import org.apache.amoro.server.util.KerberizedTestHelper;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Map;
+
+public class HighAvailabilityContainerTest extends KerberizedTestHelper {
+  @Test
+  public void setupKerberosAuth() throws Exception {
+    tryWithSecurityEnabled(
+        () -> {
+          try {
+            Configurations conf = new Configurations();
+            File keytab = File.createTempFile("amoro", ".keytab");
+            String principal = "amoro/[email protected]";
+
+            conf.set(
+                AmoroManagementConf.HA_ZOOKEEPER_AUTH_KEYTAB, 
keytab.getCanonicalPath().toString());
+            conf.set(AmoroManagementConf.HA_ZOOKEEPER_AUTH_PRINCIPAL, 
principal);
+            conf.set(AmoroManagementConf.HA_ZOOKEEPER_AUTH_TYPE, "KERBEROS");
+
+            HighAvailabilityContainer.setupZookeeperAuth(conf);
+            Configuration configuration = Configuration.getConfiguration();
+            AppConfigurationEntry[] entries =
+                configuration.getAppConfigurationEntry("AmoroZooKeeperClient");
+
+            Assertions.assertEquals(
+                "com.sun.security.auth.module.Krb5LoginModule", 
entries[0].getLoginModuleName());
+            Map<String, ?> options = entries[0].getOptions();
+
+            String hostname =
+                
StringUtils.lowerCase(InetAddress.getLocalHost().getCanonicalHostName());
+            Assertions.assertEquals("amoro/" + hostname + "@apache.org", 
options.get("principal"));
+            
Assertions.assertTrue(Boolean.parseBoolean(options.get("useKeyTab").toString()));
+
+            conf.set(AmoroManagementConf.HA_ZOOKEEPER_AUTH_KEYTAB, 
keytab.getName());
+            IOException e =
+                assertThrows(
+                    IOException.class, () -> 
HighAvailabilityContainer.setupZookeeperAuth(conf));
+            Assertions.assertTrue(e.getMessage().contains("does not exist"));
+          } catch (IOException e) {
+            throw new RuntimeException(e);
+          }
+        });
+  }
+}
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/util/KerberizedTestHelper.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/util/KerberizedTestHelper.java
new file mode 100644
index 000000000..9c92ce2e2
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/util/KerberizedTestHelper.java
@@ -0,0 +1,166 @@
+/*
+ * 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.amoro.server.util;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.minikdc.MiniKdc;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Properties;
+
+public abstract class KerberizedTestHelper {
+
+  private static final String clientPrincipalUser = "client";
+  private static File baseDir;
+  private static Properties kdcConf;
+  private static final String hostName = "localhost";
+  private static MiniKdc kdc;
+  protected static String krb5ConfPath;
+  private static File keytabFile;
+  protected static String testKeytab;
+  protected static String testPrincipal;
+  protected static String testSpnegoPrincipal;
+
+  @BeforeAll
+  public static void beforeAll() throws Exception {
+    baseDir = Files.createTempDirectory("kyuubi-kdc").toFile();
+    kdcConf = MiniKdc.createConf();
+    kdcConf.setProperty(MiniKdc.INSTANCE, 
KerberizedTestHelper.class.getSimpleName());
+    kdcConf.setProperty(MiniKdc.ORG_NAME, "KerberizedTestHelper");
+    kdcConf.setProperty(MiniKdc.ORG_DOMAIN, "COM");
+    kdcConf.setProperty(MiniKdc.KDC_BIND_ADDRESS, hostName);
+    kdcConf.setProperty(MiniKdc.KDC_PORT, "0");
+    kdcConf.remove(MiniKdc.DEBUG);
+
+    // Start MiniKdc
+    kdc = new MiniKdc(kdcConf, baseDir);
+    kdc.start();
+    krb5ConfPath = kdc.getKrb5conf().getAbsolutePath();
+
+    keytabFile = new File(baseDir, "kyuubi-test.keytab");
+    testKeytab = keytabFile.getAbsolutePath();
+    String tempTestPrincipal = clientPrincipalUser + "/" + hostName;
+    String tempSpnegoPrincipal = "HTTP/" + hostName;
+    kdc.createPrincipal(keytabFile, tempTestPrincipal, tempSpnegoPrincipal);
+    rewriteKrb5Conf();
+    testPrincipal = tempTestPrincipal + "@" + kdc.getRealm();
+    testSpnegoPrincipal = tempSpnegoPrincipal + "@" + kdc.getRealm();
+    System.out.println("KerberizedTest Principal: " + testPrincipal);
+    System.out.println("KerberizedTest SPNEGO Principal: " + 
testSpnegoPrincipal);
+    System.out.println("KerberizedTest Keytab: " + testKeytab);
+  }
+
+  /** In this method we rewrite krb5.conf to make kdc and client use the same 
enctypes */
+  private static void rewriteKrb5Conf() throws IOException {
+    File krb5conf = kdc.getKrb5conf();
+    StringBuilder rewriteKrb5Conf = new StringBuilder();
+    boolean rewritten = false;
+    String addedConfig =
+        addedKrb5Config("default_tkt_enctypes", "aes128-cts-hmac-sha1-96")
+            + addedKrb5Config("default_tgs_enctypes", 
"aes128-cts-hmac-sha1-96")
+            + addedKrb5Config("dns_lookup_realm", "true");
+
+    try (BufferedReader reader =
+        new BufferedReader(
+            new InputStreamReader(new FileInputStream(krb5conf), 
StandardCharsets.UTF_8))) {
+      String line;
+      while ((line = reader.readLine()) != null) {
+        if (line.contains("libdefaults")) {
+          rewritten = true;
+          
rewriteKrb5Conf.append(line).append(addedConfig).append(System.lineSeparator());
+        } else if (line.contains(hostName)) {
+          rewriteKrb5Conf
+              .append(line)
+              .append(System.lineSeparator())
+              .append(line.replace(hostName, "tcp/" + hostName))
+              .append(System.lineSeparator());
+        } else if (!line.trim().startsWith("#")) {
+          rewriteKrb5Conf.append(line).append(System.lineSeparator());
+        }
+      }
+    }
+
+    String krb5confStr;
+    if (!rewritten) {
+      krb5confStr =
+          "[libdefaults]"
+              + addedConfig
+              + System.lineSeparator()
+              + System.lineSeparator()
+              + rewriteKrb5Conf;
+    } else {
+      krb5confStr = rewriteKrb5Conf.toString();
+    }
+
+    // Overwrite krb5.conf
+    try (BufferedWriter writer =
+        Files.newBufferedWriter(krb5conf.toPath(), StandardCharsets.UTF_8)) {
+      writer.write(krb5confStr);
+    }
+    System.out.println("krb5.conf file content: " + krb5confStr);
+  }
+
+  private static String addedKrb5Config(String key, String value) {
+    return System.lineSeparator() + "    " + key + "=" + value;
+  }
+
+  @AfterAll
+  public static void afterAll() {
+    if (kdc != null) {
+      kdc.stop();
+    }
+    if (baseDir != null) {
+      FileUtils.deleteQuietly(baseDir);
+    }
+  }
+
+  // Usage: tryWithSecurityEnabled(() -> { /* test code */ });
+  public static void tryWithSecurityEnabled(Runnable block) throws Exception {
+    Configuration conf = new Configuration();
+    Assertions.assertFalse(UserGroupInformation.isSecurityEnabled());
+    UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
+    String authType = "hadoop.security.authentication";
+    try {
+      conf.set(authType, "KERBEROS");
+      conf.set("hadoop.security.auth_to_local", "DEFAULT RULE:[1:$1] 
RULE:[2:$1]");
+      System.setProperty("java.security.krb5.conf", krb5ConfPath);
+      UserGroupInformation.setConfiguration(conf);
+      Assertions.assertTrue(UserGroupInformation.isSecurityEnabled());
+      block.run();
+    } finally {
+      conf.unset(authType);
+      System.clearProperty("java.security.krb5.conf");
+      UserGroupInformation.setLoginUser(currentUser);
+      UserGroupInformation.setConfiguration(conf);
+      Assertions.assertFalse(UserGroupInformation.isSecurityEnabled());
+    }
+  }
+}
diff --git a/docs/configuration/ams-config.md b/docs/configuration/ams-config.md
index a67ed282c..fa763689a 100644
--- a/docs/configuration/ams-config.md
+++ b/docs/configuration/ams-config.md
@@ -73,6 +73,9 @@ table td:last-child, table th:last-child { width: 40%; 
word-break: break-all; }
 | ha.enabled | false | Whether to enable high availability mode. |
 | ha.session-timeout | 30 s | The Zookeeper session timeout in milliseconds. |
 | ha.zookeeper-address |  | The Zookeeper address used for high availability. |
+| ha.zookeeper-auth-keytab |  | The Zookeeper authentication keytab file path 
when auth type is KERBEROS. |
+| ha.zookeeper-auth-principal |  | The Zookeeper authentication principal when 
auth type is KERBEROS. |
+| ha.zookeeper-auth-type | NONE | The Zookeeper authentication type, NONE or 
KERBEROS. |
 | http-server.auth-basic-provider | 
org.apache.amoro.server.authentication.DefaultPasswdAuthenticationProvider | 
User-defined password authentication implementation of 
org.apache.amoro.authentication.PasswdAuthenticationProvider |
 | http-server.auth-jwt-provider | &lt;undefined&gt; | User-defined JWT (JSON 
Web Token) authentication implementation of 
org.apache.amoro.authentication.TokenAuthenticationProvider |
 | http-server.bind-port | 19090 | Port that the Http server is bound to. |
diff --git a/pom.xml b/pom.xml
index c0793b807..0d9f6253a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -463,6 +463,40 @@
                 </exclusions>
             </dependency>
 
+            <dependency>
+                <groupId>org.apache.hadoop</groupId>
+                <artifactId>hadoop-minikdc</artifactId>
+                <version>${hadoop.version}</version>
+                <scope>test</scope>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.slf4j</groupId>
+                        <artifactId>*</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>junit</groupId>
+                        <artifactId>junit</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>org.apache.directory.jdbm</groupId>
+                        <artifactId>apacheds-jdbm1</artifactId>
+                    </exclusion>
+                    <!-- HADOOP-19024: replace bcprov-jdk15on with 
bcprov-jdk18on -->
+                    <exclusion>
+                        <groupId>org.bouncycastle</groupId>
+                        <artifactId>bcprov-jdk15on</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <!-- apacheds-jdbm1 is required by hadoop-minikdc testing -->
+            <dependency>
+                <groupId>org.apache.directory.jdbm</groupId>
+                <artifactId>apacheds-jdbm1</artifactId>
+                <version>2.0.0-M2</version>
+                <scope>test</scope>
+            </dependency>
+
             <dependency>
                 <groupId>org.slf4j</groupId>
                 <artifactId>slf4j-api</artifactId>

Reply via email to