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 | <undefined> | 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>