This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new eec38cd54e1 [fix](kerberos) Preserve catalog realm mappings in shared
Hadoop runtimes (#69608)
eec38cd54e1 is described below
commit eec38cd54e10a2cf8972afcdace05e48aca1549e
Author: Gabriel <[email protected]>
AuthorDate: Mon Oct 26 16:17:02 2026 +0800
[fix](kerberos) Preserve catalog realm mappings in shared Hadoop runtimes
(#69608)
### What problem does this PR solve?
Issue Number: N/A
Problem Summary:
The external regression run `Doris_x64_Master_Doris_Regression_External
#1122` accessed Docker Kerberos catalogs before EMR catalogs in the same
FE process. Three suites created and switched their catalogs
successfully, then failed at line 43 (`USE`) while initializing external
metadata:
| Failed suite | Catalog path | Failure point |
| --- | --- | --- |
| `test_hive_migrate_partition_iceberg` | Iceberg HMS | Build the
Iceberg catalog |
| `test_hive_migrate_iceberg` | Iceberg HMS | Build the Iceberg catalog
|
| `test_orc_merge_io_input_streams` | Hive HMS | Create the HMS client |
All three FE stacks end in `getUGIFromSubject ->
HadoopLoginModule.commit -> KerberosName.getShortName ->
NoMatchingRule`. Their catalog properties contained explicit rules for
the EMR principal, but the shared Hadoop mapping state did not match it.
Each suite aborted before its migration-table or ORC queries ran.
Hadoop UGI and `KerberosName` are shared across FE connector plugins,
while the old `configuredAuthMethod` flag belonged to each loaded
authenticator class. After that class's first initialization, later
calls skipped configuration updates entirely, including valid
`auth_to_local` rules; the authentication-method comparison only
controlled a warning. A different plugin's first initialization could
also replace the shared rules. This explains both later-realm failures
and initialization-order dependence. The logs establish the failure
location and suite order; controlled tests reproduce both mechanisms
without assuming which catalog first published the global rules.
The same run's later `test_hive_migrate_paimon` succeeded with the EMR
configuration. This is consistent with a separate plugin's first
initialization replacing shared state, rather than demonstrating that
the failed suites had invalid credentials.
The fix:
- Share the Kerberos helper through the connector and filesystem parent
loaders. Coordinate configuration and UGI construction on
`UserGroupInformation.class`, inspecting actual Hadoop state and
tracking failed full initialization so a corrected catalog can retry
even if Hadoop already changed its authentication mode.
- Append compatible explicit Hadoop mapping rules after existing rules,
including `DEFAULT`, preserving existing first-match precedence.
Canonicalize and deduplicate rules so repeated ticket refreshes do not
accumulate copies.
- Check `isInitialized()` before the lazy security predicate so the
first catalog initializes Hadoop's group mapping service from its own
configuration. Restore previous rules and mechanism if configuration or
principal mapping fails; publish rule extensions only after successful
UGI construction.
- Validate mapping syntax, regular expressions, and mechanisms before
initializing UGI. Reject incompatible mechanisms and MIT rule extensions
without publishing changes: MIT accepts unmatched principals unchanged,
so extending its rules could rename existing users.
- Route the filesystem authenticator through the same initialization
logic and protect its initial and refreshed UGI construction with the
same monitor. JAAS/keytab login remains outside the shared monitor.
Mapping policy remains shared within each Hadoop runtime. Existing
mappings take precedence over overlapping later rules; independent
conflicting policies require separate Hadoop runtimes or a common policy
configured from startup.
### Release note
Fix `NoMatchingRule` failures when Kerberos external catalogs for
different realms share an FE Hadoop runtime.
### Check List (For Author)
- Test
- [x] Unit Test: related Kerberos and HDFS authenticator suites,
including multiple realms in both orders, independent plugin
classloaders, concurrent initialization/refresh, rule deduplication,
filesystem ticket refresh, and invalid configurations, failed principal
mapping rollback, first-catalog group settings, corrected initialization
retries, and actual connector/filesystem loader policies. 76 tests
passed with no failures or errors (30 in fe-kerberos, 31 in hdfs-base, 5
existing HDFS authenticator tests, and 10 plugin-manager tests).
- [x] Manual test: replay the original Docker/EMR mapping configurations
against Hadoop JARs from the failing build with the modified
authenticator; both realm sequences and separate plugin copies succeed.
- FE Checkstyle: full reactor and the three affected modules passed.
- The complete external regression pipeline and environment-dependent
KDC/HDFS integration tests were not run locally.
- Behavior changed:
- [x] Yes. Later compatible realm rules are retained alongside earlier
mappings; incompatible rule mechanisms fail explicitly.
- Does this need documentation?
- [x] No. No new catalog properties or configuration options.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../doris/connector/ConnectorPluginManager.java | 3 +-
.../apache/doris/fs/FileSystemPluginManager.java | 5 +-
.../connector/ConnectorPluginHadoopPatchTest.java | 51 +++
.../doris/fs/FileSystemPluginManagerTest.java | 17 +
.../hdfs/KerberosHadoopAuthenticator.java | 59 +---
.../hdfs/KerberosHadoopAuthenticatorTest.java | 129 +++++++
.../kerberos/HadoopKerberosAuthenticator.java | 134 ++++++--
.../kerberos/HadoopKerberosAuthenticatorTest.java | 370 +++++++++++++++++++++
8 files changed, 694 insertions(+), 74 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/connector/ConnectorPluginManager.java
b/fe/fe-core/src/main/java/org/apache/doris/connector/ConnectorPluginManager.java
index 4eddb056346..e660a042585 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/connector/ConnectorPluginManager.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/connector/ConnectorPluginManager.java
@@ -122,9 +122,10 @@ public class ConnectorPluginManager {
// of hadoop-common in the plugin directory (see
be-java-extensions/jni-bootstrap PluginRuntime).
//
// Package-private so ConnectorPluginHadoopPatchTest asserts against this
list, not a copy of it.
+ // Kerberos setup failure state must share Hadoop's loader so another
plugin can retry partial setup.
static final List<String> CONNECTOR_PARENT_FIRST_PREFIXES =
Arrays.asList("org.apache.doris.connector.",
"org.apache.doris.filesystem.",
- "org.apache.hadoop.");
+ "org.apache.doris.kerberos.", "org.apache.hadoop.");
/** Family label in the process-wide {@link PluginRegistry}. */
private static final String PLUGIN_FAMILY = "CONNECTOR";
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/fs/FileSystemPluginManager.java
b/fe/fe-core/src/main/java/org/apache/doris/fs/FileSystemPluginManager.java
index 4c3d5954830..08534c04957 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/fs/FileSystemPluginManager.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/fs/FileSystemPluginManager.java
@@ -86,9 +86,10 @@ public class FileSystemPluginManager {
// different Class objects for the type ExecutionAuthenticator used in
the signature"
// (and cross-loader catch blocks for StoragePropertiesException would
silently stop
// matching). Foundation is a shared contract layer by definition —
always parent-first.
- private static final List<String> FS_PARENT_FIRST_PREFIXES =
+ // Share Kerberos setup failure state with connectors that use the same
parent-loaded Hadoop runtime.
+ static final List<String> FS_PARENT_FIRST_PREFIXES =
Arrays.asList("org.apache.doris.filesystem.",
"org.apache.doris.foundation.",
- "software.amazon.awssdk.", "org.apache.hadoop.");
+ "org.apache.doris.kerberos.", "software.amazon.awssdk.",
"org.apache.hadoop.");
/** Family label in the process-wide {@link PluginRegistry}. */
private static final String PLUGIN_FAMILY = "FILESYSTEM";
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/connector/ConnectorPluginHadoopPatchTest.java
b/fe/fe-core/src/test/java/org/apache/doris/connector/ConnectorPluginHadoopPatchTest.java
index 4e055c2d850..20b159c1c38 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/connector/ConnectorPluginHadoopPatchTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/connector/ConnectorPluginHadoopPatchTest.java
@@ -19,13 +19,18 @@ package org.apache.doris.connector;
import org.apache.doris.extension.loader.ClassLoadingPolicy;
import org.apache.doris.extension.loader.PluginLoader;
+import org.apache.doris.kerberos.HadoopKerberosAuthenticator;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.authentication.util.KerberosName;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
@@ -80,6 +85,44 @@ public class ConnectorPluginHadoopPatchTest {
Assertions.assertSame(pluginLoader, pluginCopy.getClassLoader());
}
+ @Test
+ public void kerberosInitializationCanRetryThroughAnotherPlugin() throws
Exception {
+ String originalRealm = System.getProperty("java.security.krb5.realm");
+ String originalKdc = System.getProperty("java.security.krb5.kdc");
+ System.setProperty("java.security.krb5.realm", "DEFAULT.EXAMPLE");
+ System.setProperty("java.security.krb5.kdc", "localhost");
+ UserGroupInformation.reset();
+ String entry =
"org/apache/doris/kerberos/HadoopKerberosAuthenticator.class";
+ Path jar = jarWith(entry, classBytes(entry, false), "kerberos.jar");
+ try (URLClassLoader first = (URLClassLoader) loaderOver(jar,
+ ConnectorPluginManager.CONNECTOR_PARENT_FIRST_PREFIXES,
getClass().getClassLoader());
+ URLClassLoader second = (URLClassLoader) loaderOver(jar,
+
ConnectorPluginManager.CONNECTOR_PARENT_FIRST_PREFIXES,
getClass().getClassLoader())) {
+ Class<?> firstAuth =
first.loadClass(HadoopKerberosAuthenticator.class.getName());
+ Class<?> secondAuth =
second.loadClass(HadoopKerberosAuthenticator.class.getName());
+ Assertions.assertSame(HadoopKerberosAuthenticator.class,
firstAuth);
+ Assertions.assertSame(firstAuth, secondAuth);
+ UserGroupInformation.setConfiguration(new Configuration(false));
+ Configuration invalid = new Configuration(false);
+ invalid.set("hadoop.security.authentication", "kerberos");
+ invalid.set("hadoop.security.auth_to_local",
"RULE:[2:$1]s/^hdfs$/old-user/");
+ invalid.set("hadoop.kerberos.min.seconds.before.relogin", "oops");
+ InvocationTargetException failure =
Assertions.assertThrows(InvocationTargetException.class,
+ () -> firstAuth.getMethod("initializeAuthConfig",
Configuration.class).invoke(null, invalid));
+ Assertions.assertInstanceOf(IllegalArgumentException.class,
failure.getCause());
+
+ Configuration corrected = new Configuration(invalid);
+ corrected.unset("hadoop.kerberos.min.seconds.before.relogin");
+ corrected.set("hadoop.security.auth_to_local",
"RULE:[2:$1]s/^hdfs$/new-user/");
+ secondAuth.getMethod("initializeAuthConfig",
Configuration.class).invoke(null, corrected);
+ Assertions.assertEquals("new-user", new
KerberosName("hdfs/[email protected]").getShortName());
+ } finally {
+ UserGroupInformation.reset();
+ restoreProperty("java.security.krb5.realm", originalRealm);
+ restoreProperty("java.security.krb5.kdc", originalKdc);
+ }
+ }
+
@Test
public void
testParentFirstFallsBackToThePluginWhenTheKernelHasNothingToOffer() throws
Exception {
// The prefix is a delegation ORDER, not an exclusive claim. It is
what keeps the entry from
@@ -103,6 +146,14 @@ public class ConnectorPluginHadoopPatchTest {
return loaderOver(vanillaFileSystemJar(), parentFirstPrefixes,
getClass().getClassLoader());
}
+ private static void restoreProperty(String name, String value) {
+ if (value == null) {
+ System.clearProperty(name);
+ } else {
+ System.setProperty(name, value);
+ }
+ }
+
private ClassLoader loaderOver(Path jar, List<String> parentFirstPrefixes,
ClassLoader parent)
throws IOException {
URL[] urls = {jar.toUri().toURL()};
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/fs/FileSystemPluginManagerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/fs/FileSystemPluginManagerTest.java
index 29feec06d54..190ac4737ec 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/fs/FileSystemPluginManagerTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/fs/FileSystemPluginManagerTest.java
@@ -18,15 +18,20 @@
package org.apache.doris.fs;
import org.apache.doris.common.util.DatasourcePrintableMap;
+import org.apache.doris.extension.loader.ClassLoadingPolicy;
+import org.apache.doris.extension.loader.PluginLoader;
import org.apache.doris.filesystem.FileSystem;
import org.apache.doris.filesystem.FileSystemType;
import org.apache.doris.filesystem.properties.FileSystemProperties;
import org.apache.doris.filesystem.properties.StorageKind;
import org.apache.doris.filesystem.spi.FileSystemProvider;
+import org.apache.doris.kerberos.HadoopKerberosAuthenticator;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.net.URL;
+import java.net.URLClassLoader;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -35,6 +40,18 @@ import java.util.Set;
public class FileSystemPluginManagerTest {
+ @Test
+ public void filesystemPluginSharesKerberosInitializationState() throws
Exception {
+ URL classes =
HadoopKerberosAuthenticator.class.getProtectionDomain().getCodeSource().getLocation();
+ PluginLoader loader = new PluginLoader(new ClassLoadingPolicy(
+
FileSystemPluginManager.FS_PARENT_FIRST_PREFIXES).toParentFirstPackages());
+ try (URLClassLoader plugin = (URLClassLoader) loader.createClassLoader(
+ new URL[] {classes}, getClass().getClassLoader())) {
+ Assertions.assertSame(HadoopKerberosAuthenticator.class,
+
plugin.loadClass(HadoopKerberosAuthenticator.class.getName()));
+ }
+ }
+
@Test
public void registerProvider_registersProviderSensitiveKeysForMasking() {
FileSystemPluginManager manager = new FileSystemPluginManager();
diff --git
a/fe/fe-filesystem/fe-filesystem-hdfs-base/src/main/java/org/apache/doris/filesystem/hdfs/KerberosHadoopAuthenticator.java
b/fe/fe-filesystem/fe-filesystem-hdfs-base/src/main/java/org/apache/doris/filesystem/hdfs/KerberosHadoopAuthenticator.java
index ac8370a6a7a..0c8e39d3bea 100644
---
a/fe/fe-filesystem/fe-filesystem-hdfs-base/src/main/java/org/apache/doris/filesystem/hdfs/KerberosHadoopAuthenticator.java
+++
b/fe/fe-filesystem/fe-filesystem-hdfs-base/src/main/java/org/apache/doris/filesystem/hdfs/KerberosHadoopAuthenticator.java
@@ -23,11 +23,10 @@ package org.apache.doris.filesystem.hdfs;
import org.apache.doris.foundation.security.ExecutionAuthenticator;
import org.apache.doris.foundation.security.KerberosTicketUtils;
+import org.apache.doris.kerberos.HadoopKerberosAuthenticator;
import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
-import org.apache.hadoop.security.UserGroupInformation.AuthenticationMethod;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -58,21 +57,14 @@ import javax.security.auth.login.LoginException;
* SASL/GSS layer falls back to the JVM-default interactive JAAS login and
fails with
* "LoginException: Cannot read from System.in".
*
- * <p>Note: {@link UserGroupInformation#setConfiguration(Configuration)}
mutates
- * JVM-global state — all UGI instances in the process share the same
authentication
- * mode. Multiple HDFS catalogs with differing {@code
hadoop.security.authentication}
- * values therefore cannot truly coexist: the first writer wins. We serialise
the
- * setup via {@link #UGI_INIT_LOCK} and skip the {@code setConfiguration} call
when
- * the existing auth method already matches, so concurrent Kerberos catalogs
don't
- * stomp on each other. When modes disagree a WARN is logged so operators
notice.
+ * <p>UGI authentication and auth_to_local rules are shared with connector
plugins.
+ * {@link HadoopKerberosAuthenticator#getUGIFromSubject(Configuration,
Subject)} initializes
+ * that state and retains later catalogs' rules only after successful
principal mapping.
*/
public class KerberosHadoopAuthenticator implements ExecutionAuthenticator {
private static final Logger LOG =
LogManager.getLogger(KerberosHadoopAuthenticator.class);
- /** Process-wide lock for serialising UGI static setup. */
- private static final Object UGI_INIT_LOCK = new Object();
-
private final String principal;
private final String keytab;
@@ -88,15 +80,9 @@ public class KerberosHadoopAuthenticator implements
ExecutionAuthenticator {
this.principal = principal;
this.keytab = keytab;
try {
- synchronized (UGI_INIT_LOCK) {
- AuthenticationMethod desired =
SecurityUtil.getAuthenticationMethod(conf);
- if (!shouldSkipSetConfiguration(desired)) {
- UserGroupInformation.setConfiguration(conf);
- }
- this.subject = loginSubject();
- this.ugi =
Objects.requireNonNull(UserGroupInformation.getUGIFromSubject(subject),
- "getUGIFromSubject returned null");
- }
+ this.subject = loginSubject();
+ this.ugi =
Objects.requireNonNull(HadoopKerberosAuthenticator.getUGIFromSubject(conf,
subject),
+ "getUGIFromSubject returned null");
this.nextRefreshTime = KerberosTicketUtils.getRefreshTime(
KerberosTicketUtils.getTicketGrantingTicket(subject));
LOG.info("Kerberos login succeeded for principal={}", principal);
@@ -106,31 +92,6 @@ public class KerberosHadoopAuthenticator implements
ExecutionAuthenticator {
}
}
- /**
- * Returns true when the JVM-global UGI configuration already matches what
this
- * authenticator needs, so we can safely skip the {@code setConfiguration}
call.
- * If security is on but the existing auth mode differs, a WARN is logged
and
- * we still skip to preserve first-writer-wins semantics — the operator is
then
- * responsible for aligning catalog configs.
- */
- private boolean shouldSkipSetConfiguration(AuthenticationMethod desired) {
- if (!UserGroupInformation.isSecurityEnabled()) {
- return false;
- }
- AuthenticationMethod current;
- try {
- current =
UserGroupInformation.getLoginUser().getAuthenticationMethod();
- } catch (IOException e) {
- return false;
- }
- if (current == desired) {
- return true;
- }
- LOG.warn("UGI already configured with authentication={} but this
catalog requests {}; "
- + "keeping existing JVM-global setting (first-writer-wins).",
current, desired);
- return true;
- }
-
@Override
public <T> T execute(Callable<T> task) throws Exception {
UserGroupInformation currentUgi;
@@ -161,8 +122,10 @@ public class KerberosHadoopAuthenticator implements
ExecutionAuthenticator {
private synchronized UserGroupInformation getUGI() throws IOException {
if (nextRefreshTime < System.currentTimeMillis()) {
Subject newSubject = loginSubject();
-
Objects.requireNonNull(UserGroupInformation.getUGIFromSubject(newSubject),
- "getUGIFromSubject returned null");
+ synchronized (UserGroupInformation.class) {
+
Objects.requireNonNull(UserGroupInformation.getUGIFromSubject(newSubject),
+ "getUGIFromSubject returned null");
+ }
// We modify the existing UGI's credentials in-place instead of
returning a new UGI
// because some parts of Hadoop code reuse UGI (e.g. DFSClient).
// We also need to clear the old credentials because the JDK
assumes that the first
diff --git
a/fe/fe-filesystem/fe-filesystem-hdfs-base/src/test/java/org/apache/doris/filesystem/hdfs/KerberosHadoopAuthenticatorTest.java
b/fe/fe-filesystem/fe-filesystem-hdfs-base/src/test/java/org/apache/doris/filesystem/hdfs/KerberosHadoopAuthenticatorTest.java
new file mode 100644
index 00000000000..1b3dfe9dee7
--- /dev/null
+++
b/fe/fe-filesystem/fe-filesystem-hdfs-base/src/test/java/org/apache/doris/filesystem/hdfs/KerberosHadoopAuthenticatorTest.java
@@ -0,0 +1,129 @@
+// 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.doris.filesystem.hdfs;
+
+import org.apache.doris.kerberos.HadoopKerberosAuthenticator;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Date;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.kerberos.KerberosTicket;
+
+class KerberosHadoopAuthenticatorTest {
+ private String originalRealm;
+ private String originalKdc;
+
+ @BeforeEach
+ void setUp() {
+ originalRealm = System.getProperty("java.security.krb5.realm");
+ originalKdc = System.getProperty("java.security.krb5.kdc");
+ System.setProperty("java.security.krb5.realm", "DEFAULT.EXAMPLE");
+ System.setProperty("java.security.krb5.kdc", "localhost");
+ UserGroupInformation.reset();
+ UserGroupInformation.setConfiguration(configuration("FIRST.EXAMPLE"));
+ }
+
+ @AfterEach
+ void tearDown() {
+ UserGroupInformation.reset();
+ restoreProperty("java.security.krb5.realm", originalRealm);
+ restoreProperty("java.security.krb5.kdc", originalKdc);
+ }
+
+ @Test
+ void filesystemAndConnectorRealmsSurviveTicketRefresh() throws Exception {
+ KerberosHadoopAuthenticator first = authenticator("FIRST.EXAMPLE", new
AtomicInteger());
+ AtomicInteger secondLogins = new AtomicInteger();
+ KerberosHadoopAuthenticator second = Assertions.assertDoesNotThrow(
+ () -> authenticator("SECOND.EXAMPLE", secondLogins));
+ Assertions.assertEquals(1, secondLogins.get());
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("THIRD.EXAMPLE"));
+
+ // The fabricated TGTs have expired, so execute() must rebuild the UGI
before running the action.
+ Assertions.assertEquals("hdfs/[email protected]",
+ first.execute(() ->
UserGroupInformation.getCurrentUser().getUserName()));
+ Assertions.assertEquals("hdfs/[email protected]",
+ second.execute(() ->
UserGroupInformation.getCurrentUser().getUserName()));
+ Assertions.assertEquals(2, secondLogins.get());
+ Subject third = subject("THIRD.EXAMPLE");
+ Assertions.assertEquals("hdfs",
UserGroupInformation.getUGIFromSubject(third).getShortUserName());
+ }
+
+ @Test
+ void failedFilesystemMappingDoesNotPoisonCorrectedCatalogs() throws
Exception {
+ Configuration invalid = configuration("SECOND.EXAMPLE");
+ invalid.set("hadoop.security.auth_to_local", "RULE:[2:$3]");
+ String rules = KerberosName.getRules();
+ Assertions.assertThrows(RuntimeException.class, () -> new
KerberosHadoopAuthenticator(
+ "hdfs/[email protected]", "unused.keytab", invalid) {
+ @Override
+ Subject loginSubject() {
+ return subject("SECOND.EXAMPLE");
+ }
+ });
+ Assertions.assertEquals(rules, KerberosName.getRules());
+ KerberosHadoopAuthenticator corrected =
authenticator("SECOND.EXAMPLE", new AtomicInteger());
+ Assertions.assertEquals("hdfs",
+ corrected.execute(() ->
UserGroupInformation.getCurrentUser().getShortUserName()));
+ Assertions.assertEquals("hdfs", HadoopKerberosAuthenticator
+ .getUGIFromSubject(configuration("THIRD.EXAMPLE"),
subject("THIRD.EXAMPLE")).getShortUserName());
+ }
+
+ private static KerberosHadoopAuthenticator authenticator(String realm,
AtomicInteger logins) {
+ return new KerberosHadoopAuthenticator("hdfs/host@" + realm,
"unused.keytab", configuration(realm)) {
+ @Override
+ Subject loginSubject() {
+ logins.incrementAndGet();
+ return subject(realm);
+ }
+ };
+ }
+
+ private static Subject subject(String realm) {
+ KerberosPrincipal client = new KerberosPrincipal("hdfs/host@" + realm);
+ KerberosTicket ticket = new KerberosTicket(new byte[] {1}, client,
+ new KerberosPrincipal("krbtgt/" + realm + "@" + realm), new
byte[8], 1,
+ null, null, new Date(1000L), new Date(2000L), null, null);
+ return new Subject(false, Set.of(client), Set.of(), Set.of(ticket));
+ }
+
+ private static Configuration configuration(String realm) {
+ Configuration conf = new Configuration(false);
+ conf.set("hadoop.security.authentication", "kerberos");
+ conf.set("hadoop.security.auth_to_local", "RULE:[2:$1@$0](.*@" + realm
+ ")s/@.*// DEFAULT");
+ return conf;
+ }
+
+ private static void restoreProperty(String name, String value) {
+ if (value == null) {
+ System.clearProperty(name);
+ } else {
+ System.setProperty(name, value);
+ }
+ }
+}
diff --git
a/fe/fe-kerberos/src/main/java/org/apache/doris/kerberos/HadoopKerberosAuthenticator.java
b/fe/fe-kerberos/src/main/java/org/apache/doris/kerberos/HadoopKerberosAuthenticator.java
index 599f3dbb995..4145264ec68 100644
---
a/fe/fe-kerberos/src/main/java/org/apache/doris/kerberos/HadoopKerberosAuthenticator.java
+++
b/fe/fe-kerberos/src/main/java/org/apache/doris/kerberos/HadoopKerberosAuthenticator.java
@@ -22,17 +22,20 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
-import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.authentication.util.KerberosName;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
+import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.kerberos.KerberosTicket;
@@ -47,9 +50,14 @@ public class HadoopKerberosAuthenticator implements
HadoopAuthenticator {
private long nextRefreshTime;
private UserGroupInformation ugi;
- // The authentication method the first caller published to the
process-wide UGI configuration.
- // Guarded by HadoopKerberosAuthenticator.class; null until the first
setConfiguration.
- private static UserGroupInformation.AuthenticationMethod
configuredAuthMethod = null;
+ // Shared with Hadoop through the plugin parent loader; guarded by
UserGroupInformation.class.
+ // Hadoop can publish its auth mode before setConfiguration fails, so that
mode alone cannot prove success.
+ private static boolean authConfigurationFailed;
+
+ // Hadoop's rule grammar keeps whitespace inside a regex or substitution
distinct from rule separators.
+ private static final Pattern AUTH_TO_LOCAL_RULE = Pattern.compile(
+ "\\s*((DEFAULT)|(RULE:\\[(\\d*):([^\\]]*)](\\(([^)]*)\\))?"
+ + "(s/([^/]*)/([^/]*)/(g)?)?))/?(L)?");
public HadoopKerberosAuthenticator(KerberosAuthenticationConfig config) {
this.config = config;
@@ -57,26 +65,107 @@ public class HadoopKerberosAuthenticator implements
HadoopAuthenticator {
public static void initializeAuthConfig(Configuration hadoopConf) {
hadoopConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHORIZATION,
"true");
- // UserGroupInformation.setConfiguration mutates a single process-wide
global, so multiple HDFS
- // catalogs with differing hadoop.security.authentication cannot truly
coexist. First-writer-wins:
- // only the first caller publishes the global config; later catalogs
(and every ticket refresh,
- // which re-enters this method via login()) skip it so they don't
stomp on each other. A WARN
- // fires only on a genuine auth-method mismatch. We compare against a
remembered method rather than
- // UserGroupInformation.getLoginUser() on purpose: Doris never
establishes a process-wide login user
- // (per-instance getUGIFromSubject only), and getLoginUser() would
trigger exactly that.
- UserGroupInformation.AuthenticationMethod desired =
SecurityUtil.getAuthenticationMethod(hadoopConf);
- synchronized (HadoopKerberosAuthenticator.class) {
- if (configuredAuthMethod == null) {
- UserGroupInformation.setConfiguration(hadoopConf);
- configuredAuthMethod = desired;
+ String additionalRules =
hadoopConf.get(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTH_TO_LOCAL);
+ String mechanism =
hadoopConf.get(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTH_TO_LOCAL_MECHANISM,
+ KerberosName.DEFAULT_MECHANISM);
+ // UGI changes its auth mode before parsing rules. Reject malformed
input before it can leave
+ // an enabled UGI with the permissive rules inherited from a previous
SIMPLE initialization.
+
Preconditions.checkArgument(KerberosName.MECHANISM_HADOOP.equalsIgnoreCase(mechanism)
+ ||
KerberosName.MECHANISM_MIT.equalsIgnoreCase(mechanism),
+ "Invalid auth_to_local mechanism: %s", mechanism);
+ Set<String> requestedRules = additionalRules == null
+ ? Collections.emptySet() :
parseAuthToLocalRules(additionalRules);
+ // Connector and filesystem plugins share Hadoop and this helper
through their parent loader.
+ // Use Hadoop's monitor for both policy changes and the principal
mapping that validates them.
+ synchronized (UserGroupInformation.class) {
+ // isSecurityEnabled() lazily initializes UGI and its Groups
singleton with default settings.
+ if (!UserGroupInformation.isInitialized() ||
authConfigurationFailed
+ || !UserGroupInformation.isSecurityEnabled()) {
+ String previousRules = KerberosName.getRules();
+ String previousMechanism = KerberosName.getRuleMechanism();
+ try {
+ UserGroupInformation.setConfiguration(hadoopConf);
+ authConfigurationFailed = false;
+ } catch (RuntimeException e) {
+ authConfigurationFailed = true;
+ KerberosName.setRules(previousRules);
+ KerberosName.setRuleMechanism(previousMechanism);
+ throw e;
+ }
return;
}
- if (configuredAuthMethod != desired) {
- LOG.warn("UGI already configured with authentication={} but
this catalog requests {}; "
- + "keeping existing JVM-global setting
(first-writer-wins).",
- configuredAuthMethod, desired);
+ if (additionalRules == null) {
+ return;
+ }
+
Preconditions.checkArgument(mechanism.equalsIgnoreCase(KerberosName.getRuleMechanism()),
+ "Kerberos catalogs sharing a Hadoop runtime must use the
same auth_to_local mechanism "
+ + "(existing=%s, requested=%s)",
KerberosName.getRuleMechanism(), mechanism);
+ Set<String> rules = parseAuthToLocalRules(KerberosName.getRules());
+ int previousSize = rules.size();
+ // Append after all existing rules, including DEFAULT: a later
realm must not change an
+ // earlier principal's mapping. Replacing the global configuration
would break its next login.
+ if (rules.addAll(requestedRules)) {
+ // MIT accepts an unmatched principal unchanged; extending its
rules could rename a user.
+
Preconditions.checkArgument(KerberosName.MECHANISM_HADOOP.equalsIgnoreCase(mechanism),
+ "MIT auth_to_local rules cannot be extended after
Hadoop initialization; "
+ + "configure a common policy before
initializing catalogs");
+ KerberosName.setRules(String.join("\n", rules));
+ LOG.debug("Extended JVM-global Kerberos auth_to_local rules
from {} to {} rules",
+ previousSize, rules.size());
+ }
+ }
+ }
+
+ public static UserGroupInformation getUGIFromSubject(Configuration
hadoopConf, Subject subject) throws IOException {
+ synchronized (UserGroupInformation.class) {
+ String previousRules = KerberosName.getRules();
+ String previousMechanism = KerberosName.getRuleMechanism();
+ boolean configuring = !UserGroupInformation.isInitialized() ||
authConfigurationFailed
+ || !UserGroupInformation.isSecurityEnabled();
+ try {
+ initializeAuthConfig(hadoopConf);
+ return UserGroupInformation.getUGIFromSubject(subject);
+ } catch (IOException | RuntimeException e) {
+ // Some valid rule syntax (e.g. an out-of-range $ parameter)
fails only when a principal
+ // is mapped. A failed catalog must not leave that rule ahead
of later corrected rules.
+ KerberosName.setRules(previousRules);
+ KerberosName.setRuleMechanism(previousMechanism);
+ if (configuring) {
+ authConfigurationFailed = true;
+ }
+ throw e;
+ }
+ }
+ }
+
+ private static Set<String> parseAuthToLocalRules(String rawRules) {
+ String rules = rawRules.trim();
+ Set<String> result = new LinkedHashSet<>();
+ Matcher matcher = AUTH_TO_LOCAL_RULE.matcher(rules);
+ int position = 0;
+ while (position < rules.length()) {
+ matcher.region(position, rules.length());
+ if (!matcher.lookingAt()) {
+ throw new IllegalArgumentException("Invalid auth_to_local
rule: " + rules.substring(position));
+ }
+ String rule = matcher.group(1);
+ if (matcher.group(4) != null) {
+ if (matcher.group(7) != null) {
+ Pattern.compile(matcher.group(7));
+ }
+ if (matcher.group(9) != null) {
+ Pattern.compile(matcher.group(9));
+ }
+ rule = "RULE:[" + Integer.parseInt(matcher.group(4)) + ":" +
matcher.group(5) + "]"
+ + Objects.toString(matcher.group(6), "") +
Objects.toString(matcher.group(8), "");
+ if (matcher.group(12) != null) {
+ rule += "/L";
+ }
}
+ result.add(rule);
+ position = matcher.end();
}
+ return result;
}
@Override
@@ -132,13 +221,12 @@ public class HadoopKerberosAuthenticator implements
HadoopAuthenticator {
}
private UserGroupInformation login(Subject subject) throws IOException {
- // login and get ugi when catalog is initialized
- initializeAuthConfig(config.getConf());
+ // Login and get UGI when the catalog is initialized.
String principal = config.getKerberosPrincipal();
if (LOG.isDebugEnabled()) {
LOG.debug("Login by kerberos authentication with principal: {}",
principal);
}
- return UserGroupInformation.getUGIFromSubject(subject);
+ return getUGIFromSubject(config.getConf(), subject);
}
private static long calculateNextRefreshTime(Subject subject) {
diff --git
a/fe/fe-kerberos/src/test/java/org/apache/doris/kerberos/HadoopKerberosAuthenticatorTest.java
b/fe/fe-kerberos/src/test/java/org/apache/doris/kerberos/HadoopKerberosAuthenticatorTest.java
new file mode 100644
index 00000000000..332110824b8
--- /dev/null
+++
b/fe/fe-kerberos/src/test/java/org/apache/doris/kerberos/HadoopKerberosAuthenticatorTest.java
@@ -0,0 +1,370 @@
+// 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.doris.kerberos;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.security.Groups;
+import org.apache.hadoop.security.KerberosAuthException;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+
+class HadoopKerberosAuthenticatorTest {
+ private static final String AUTH_TO_LOCAL =
"hadoop.security.auth_to_local";
+ private static final String RULE_MECHANISM =
"hadoop.security.auth_to_local.mechanism";
+ private String originalRealm;
+ private String originalKdc;
+
+ @BeforeEach
+ void setUp() {
+ originalRealm = System.getProperty("java.security.krb5.realm");
+ originalKdc = System.getProperty("java.security.krb5.kdc");
+ System.setProperty("java.security.krb5.realm", "DEFAULT.EXAMPLE");
+ System.setProperty("java.security.krb5.kdc", "localhost");
+ UserGroupInformation.reset();
+ UserGroupInformation.setConfiguration(configuration("FIRST.EXAMPLE"));
+ }
+
+ @AfterEach
+ void tearDown() {
+ UserGroupInformation.reset();
+ restoreProperty("java.security.krb5.realm", originalRealm);
+ restoreProperty("java.security.krb5.kdc", originalKdc);
+ }
+
+ @Test
+ void laterRealmCanCreateUgiWithoutBreakingEarlierRealm() throws Exception {
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("FIRST.EXAMPLE"));
+ assertUser("FIRST.EXAMPLE");
+
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("SECOND.EXAMPLE"));
+ assertUser("SECOND.EXAMPLE");
+ assertUser("FIRST.EXAMPLE");
+ }
+
+ @Test
+ void firstKerberosCatalogInitializesSharedUgi() {
+ UserGroupInformation.reset();
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("FIRST.EXAMPLE"));
+ Assertions.assertTrue(UserGroupInformation.isSecurityEnabled());
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("SECOND.EXAMPLE"));
+ assertUser("FIRST.EXAMPLE");
+ assertUser("SECOND.EXAMPLE");
+ }
+
+ @Test
+ void firstCatalogKeepsItsGroupMappingConfiguration() throws Exception {
+ UserGroupInformation.reset();
+ Groups.reset();
+ try {
+ Configuration conf = configuration("FIRST.EXAMPLE");
+ conf.set("hadoop.user.group.static.mapping.overrides",
"hdfs=analytics");
+ HadoopKerberosAuthenticator.initializeAuthConfig(conf);
+ Assertions.assertArrayEquals(new String[] {"analytics"},
user("FIRST.EXAMPLE").getGroupNames());
+ } finally {
+ Groups.reset();
+ }
+ }
+
+ @Test
+ void failedInitialConfigurationCanBeCorrected() throws Exception {
+ UserGroupInformation.setConfiguration(new Configuration(false));
+ Configuration invalid = configuration("FIRST.EXAMPLE");
+ invalid.set(AUTH_TO_LOCAL, "RULE:[2:$1]s/^hdfs$/old-user/");
+ invalid.set("hadoop.kerberos.min.seconds.before.relogin", "oops");
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () ->
HadoopKerberosAuthenticator.initializeAuthConfig(invalid));
+
+ Configuration corrected = configuration("FIRST.EXAMPLE");
+ corrected.set(AUTH_TO_LOCAL, "RULE:[2:$1]s/^hdfs$/new-user/");
+ HadoopKerberosAuthenticator.initializeAuthConfig(corrected);
+ Assertions.assertEquals("new-user",
user("FIRST.EXAMPLE").getShortUserName());
+ }
+
+ @Test
+ void failedPrincipalMappingDoesNotPoisonLaterCatalogs() throws Exception {
+ Configuration invalid = configuration("SECOND.EXAMPLE");
+ invalid.set(AUTH_TO_LOCAL, "RULE:[2:$3]");
+ String rules = KerberosName.getRules();
+ Subject subject = new Subject(false, Set.of(new
KerberosPrincipal("hdfs/[email protected]")),
+ Set.of(), Set.of());
+ Assertions.assertThrows(KerberosAuthException.class,
+ () -> HadoopKerberosAuthenticator.getUGIFromSubject(invalid,
subject));
+ Assertions.assertEquals(rules, KerberosName.getRules());
+
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("SECOND.EXAMPLE"));
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("THIRD.EXAMPLE"));
+ assertUser("FIRST.EXAMPLE");
+ assertUser("SECOND.EXAMPLE");
+ assertUser("THIRD.EXAMPLE");
+ }
+
+ @Test
+ void failedFirstPrincipalMappingCanBeCorrected() throws Exception {
+ UserGroupInformation.reset();
+ Configuration invalid = configuration("FIRST.EXAMPLE");
+ invalid.set(AUTH_TO_LOCAL, "RULE:[2:$3]");
+ Subject subject = new Subject(false, Set.of(new
KerberosPrincipal("hdfs/[email protected]")),
+ Set.of(), Set.of());
+ Assertions.assertThrows(KerberosAuthException.class,
+ () -> HadoopKerberosAuthenticator.getUGIFromSubject(invalid,
subject));
+ Assertions.assertNull(KerberosName.getRules());
+ Assertions.assertEquals("hdfs", HadoopKerberosAuthenticator
+ .getUGIFromSubject(configuration("FIRST.EXAMPLE"),
subject).getShortUserName());
+ }
+
+ @Test
+ void reverseInitializationOrderKeepsBothRealms() {
+ UserGroupInformation.setConfiguration(configuration("SECOND.EXAMPLE"));
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("FIRST.EXAMPLE"));
+ assertUser("FIRST.EXAMPLE");
+ assertUser("SECOND.EXAMPLE");
+ }
+
+ @Test
+ void independentPluginCopiesDoNotReplaceSharedRules() throws Exception {
+ URL classes =
HadoopKerberosAuthenticator.class.getProtectionDomain().getCodeSource().getLocation();
+ try (URLClassLoader first = pluginLoader(classes); URLClassLoader
second = pluginLoader(classes)) {
+ Class<?> firstAuth =
first.loadClass(HadoopKerberosAuthenticator.class.getName());
+ Class<?> secondAuth =
second.loadClass(HadoopKerberosAuthenticator.class.getName());
+ Assertions.assertNotSame(firstAuth, secondAuth);
+
Assertions.assertSame(first.loadClass(UserGroupInformation.class.getName()),
+ second.loadClass(UserGroupInformation.class.getName()));
+ firstAuth.getMethod("initializeAuthConfig", Configuration.class)
+ .invoke(null, configuration("FIRST.EXAMPLE"));
+ secondAuth.getMethod("initializeAuthConfig", Configuration.class)
+ .invoke(null, configuration("SECOND.EXAMPLE"));
+
+ assertUser("FIRST.EXAMPLE");
+ assertUser("SECOND.EXAMPLE");
+ }
+ }
+
+ @Test
+ void concurrentInitializationAndRefreshKeepEveryRealm() throws Exception {
+ int catalogs = 8;
+ ExecutorService executor = Executors.newFixedThreadPool(catalogs);
+ CountDownLatch start = new CountDownLatch(1);
+ List<Future<?>> futures = new ArrayList<>();
+ try {
+ for (int i = 0; i < catalogs; i++) {
+ String realm = "REALM" + i + ".EXAMPLE";
+ futures.add(executor.submit(() -> {
+ Assertions.assertTrue(start.await(10, TimeUnit.SECONDS));
+ for (int refresh = 0; refresh < 20; refresh++) {
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration(realm));
+ assertUser(realm);
+ assertUser("FIRST.EXAMPLE");
+ }
+ return null;
+ }));
+ }
+ start.countDown();
+ for (Future<?> future : futures) {
+ Assertions.assertDoesNotThrow(() -> future.get(30,
TimeUnit.SECONDS));
+ }
+ } finally {
+ executor.shutdownNow();
+ Assertions.assertTrue(executor.awaitTermination(10,
TimeUnit.SECONDS));
+ }
+ }
+
+ @Test
+ void refreshDoesNotAccumulateDuplicateRules() throws Exception {
+ Configuration conf = configuration("SECOND.EXAMPLE");
+ conf.set(AUTH_TO_LOCAL, " RULE:[02:$1@$0](.*@SECOND.EXAMPLE)s/@.*//
DEFAULT ");
+ HadoopKerberosAuthenticator.initializeAuthConfig(conf);
+ String rules = KerberosName.getRules();
+ assertUser("SECOND.EXAMPLE");
+ for (int refresh = 0; refresh < 20; refresh++) {
+ HadoopKerberosAuthenticator.initializeAuthConfig(conf);
+ }
+ Assertions.assertEquals(rules, KerberosName.getRules());
+ Assertions.assertEquals(3, rules.lines().count());
+ }
+
+ @Test
+ void lowercaseFlagsAndDefaultRulesAreCanonicalized() {
+ Configuration conf = configuration("SECOND.EXAMPLE");
+ conf.set(AUTH_TO_LOCAL, "RULE:[02:$1@$0](.*@SECOND.EXAMPLE)s/@.*//g/L
DEFAULT/L");
+ for (int refresh = 0; refresh < 20; refresh++) {
+ HadoopKerberosAuthenticator.initializeAuthConfig(conf);
+ }
+ Assertions.assertEquals(3, KerberosName.getRules().lines().count());
+ assertUser("FIRST.EXAMPLE");
+ assertUser("SECOND.EXAMPLE");
+ }
+
+ @Test
+ void whitespaceInsideSubstitutionIsPreserved() throws Exception {
+ Configuration conf = configuration("SECOND.EXAMPLE");
+ conf.set(AUTH_TO_LOCAL,
+ "RULE:[2:$1@$0](.*@SECOND.EXAMPLE)s/(hdfs|user
name)@.*/mapped-user/ DEFAULT");
+ HadoopKerberosAuthenticator.initializeAuthConfig(conf);
+ Assertions.assertEquals("mapped-user",
user("SECOND.EXAMPLE").getShortUserName());
+ String rules = KerberosName.getRules();
+ HadoopKerberosAuthenticator.initializeAuthConfig(conf);
+ Assertions.assertEquals(rules, KerberosName.getRules());
+ }
+
+ @Test
+ void extendingMitRulesIsRejectedWithoutChangingFallbackMappings() throws
Exception {
+ Configuration first = configuration("FIRST.EXAMPLE");
+ first.set(RULE_MECHANISM, "mit");
+ UserGroupInformation.setConfiguration(first);
+ Configuration second = configuration("SECOND.EXAMPLE");
+ second.set(RULE_MECHANISM, "mit");
+ String rules = KerberosName.getRules();
+ String originalUser = user("SECOND.EXAMPLE").getShortUserName();
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () ->
HadoopKerberosAuthenticator.initializeAuthConfig(second));
+ Assertions.assertEquals(rules, KerberosName.getRules());
+ Assertions.assertEquals(originalUser,
user("SECOND.EXAMPLE").getShortUserName());
+ Assertions.assertDoesNotThrow(() ->
HadoopKerberosAuthenticator.initializeAuthConfig(first));
+ }
+
+ @Test
+ void existingRulesKeepTheirPrecedence() throws Exception {
+ Configuration conf = configuration("FIRST.EXAMPLE");
+ conf.set(AUTH_TO_LOCAL,
"RULE:[2:$1@$0](.*@FIRST.EXAMPLE)s/.*/another-user/ DEFAULT");
+ HadoopKerberosAuthenticator.initializeAuthConfig(conf);
+
+ assertUser("FIRST.EXAMPLE");
+ Assertions.assertThrows(KerberosAuthException.class, () ->
user("UNCONFIGURED.EXAMPLE"));
+ }
+
+ @Test
+ void invalidRulesDoNotChangePublishedRules() {
+ Configuration conf = configuration("SECOND.EXAMPLE");
+ conf.set(AUTH_TO_LOCAL, "INVALID");
+ String rules = KerberosName.getRules();
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> HadoopKerberosAuthenticator.initializeAuthConfig(conf));
+ Assertions.assertEquals(rules, KerberosName.getRules());
+ }
+
+ @Test
+ void invalidFirstCatalogCannotPublishPermissiveFallback() {
+ Configuration invalidSyntax = configuration("FIRST.EXAMPLE");
+ invalidSyntax.set(AUTH_TO_LOCAL, "INVALID");
+ Configuration invalidRegex = configuration("FIRST.EXAMPLE");
+ invalidRegex.set(AUTH_TO_LOCAL, "RULE:[2:$1@$0]s/[/x/");
+ Configuration invalidMechanism = configuration("FIRST.EXAMPLE");
+ invalidMechanism.set(RULE_MECHANISM, "unknown");
+ for (Configuration invalid : List.of(invalidSyntax, invalidRegex,
invalidMechanism)) {
+ UserGroupInformation.reset();
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () ->
HadoopKerberosAuthenticator.initializeAuthConfig(invalid));
+ Assertions.assertFalse(UserGroupInformation.isSecurityEnabled());
+
HadoopKerberosAuthenticator.initializeAuthConfig(configuration("SECOND.EXAMPLE"));
+ assertUser("SECOND.EXAMPLE");
+ Assertions.assertThrows(KerberosAuthException.class, () ->
user("FIRST.EXAMPLE"));
+ }
+ }
+
+ @Test
+ void incompatibleRuleMechanismDoesNotChangePublishedRules() {
+ Configuration conf = configuration("SECOND.EXAMPLE");
+ conf.set(RULE_MECHANISM, "mit");
+ String rules = KerberosName.getRules();
+
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> HadoopKerberosAuthenticator.initializeAuthConfig(conf));
+ Assertions.assertEquals(rules, KerberosName.getRules());
+ Assertions.assertEquals("hadoop", KerberosName.getRuleMechanism());
+ }
+
+ @Test
+ void absentRulesDoNotBroadenExistingPolicy() throws Exception {
+ Configuration conf = configuration("SECOND.EXAMPLE");
+ conf.unset(AUTH_TO_LOCAL);
+ String rules = KerberosName.getRules();
+ HadoopKerberosAuthenticator.initializeAuthConfig(conf);
+
+ Assertions.assertEquals(rules, KerberosName.getRules());
+ assertUser("FIRST.EXAMPLE");
+ Assertions.assertThrows(KerberosAuthException.class, () ->
user("SECOND.EXAMPLE"));
+ }
+
+ private static Configuration configuration(String realm) {
+ Configuration conf = new Configuration(false);
+ conf.set("hadoop.security.authentication", "kerberos");
+ conf.set(AUTH_TO_LOCAL, "RULE:[2:$1@$0](.*@" + realm +
")s/@.*//\nDEFAULT");
+ return conf;
+ }
+
+ private static void assertUser(String realm) {
+ UserGroupInformation ugi = Assertions.assertDoesNotThrow(() ->
user(realm));
+ Assertions.assertEquals("hdfs", ugi.getShortUserName());
+ }
+
+ private static UserGroupInformation user(String realm) throws Exception {
+ Subject subject = new Subject(false, Set.of(new
KerberosPrincipal("hdfs/host@" + realm)),
+ Set.of(), Set.of());
+ synchronized (UserGroupInformation.class) {
+ return UserGroupInformation.getUGIFromSubject(subject);
+ }
+ }
+
+ private static URLClassLoader pluginLoader(URL classes) {
+ return new URLClassLoader(new URL[] {classes},
HadoopKerberosAuthenticatorTest.class.getClassLoader()) {
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws
ClassNotFoundException {
+ synchronized (getClassLoadingLock(name)) {
+ if (name.startsWith("org.apache.doris.kerberos.")) {
+ Class<?> loaded = findLoadedClass(name);
+ if (loaded == null) {
+ loaded = findClass(name);
+ }
+ if (resolve) {
+ resolveClass(loaded);
+ }
+ return loaded;
+ }
+ return super.loadClass(name, resolve);
+ }
+ }
+ };
+ }
+
+ private static void restoreProperty(String name, String value) {
+ if (value == null) {
+ System.clearProperty(name);
+ } else {
+ System.setProperty(name, value);
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]