headers = new LinkedHashMap<>();
+
+ for (String headerName : headerNames) {
+ headers.put(headerName, spiffeId);
+ }
+
+ return Collections.unmodifiableMap(headers);
+ }
+
+ private static String resolveSpiffeHeaderName(final Properties props,
+ final String configPrefix) {
+ String headerName = props != null
+ ? StringUtils.trimToNull(
+ props.getProperty(configPrefix + "." + PROP_HEADER_SPIFFE))
+ : null;
+
+ return headerName != null ? headerName : DEFAULT_SPIFFE_HEADER_NAME;
+ }
+}
diff --git a/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdentityResolver.java b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdentityResolver.java
new file mode 100644
index 00000000000..8d470d3cedf
--- /dev/null
+++ b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdentityResolver.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ranger.plugin.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * Resolves a workload SPIFFE ID from plugin/site configuration.
+ *
+ * Resolution order: explicit {@code authn.spiffe.value}, identity file
+ * ({@code authn.spiffe.file} or the default SPIRE path), then {@code SPIFFE_ID}
+ * environment variable.
+ */
+public final class SpiffeIdentityResolver {
+ private static final Logger LOG = LoggerFactory.getLogger(SpiffeIdentityResolver.class);
+
+ public static final String PROP_SPIFFE_VALUE = "authn.spiffe.value";
+ public static final String PROP_SPIFFE_FILE = "authn.spiffe.file";
+ public static final String ENV_SPIFFE_ID = "SPIFFE_ID";
+ public static final String DEFAULT_SPIFFE_IDENTITY_FILE = "/var/run/secrets/spiffe.io/identity/spiffe";
+
+ private SpiffeIdentityResolver() {
+ // to block instantiation
+ }
+
+ /**
+ * Resolves the SPIFFE ID for the given config prefix.
+ *
+ * @param props plugin or site configuration properties
+ * @param configPrefix prefix such as {@code ranger.hive}
+ * @return the resolved SPIFFE ID, or {@code null} when unavailable
+ */
+ public static String resolve(final Properties props, final String configPrefix) {
+ String ret = null;
+
+ if (props != null && StringUtils.isNotBlank(configPrefix)) {
+ ret = StringUtils.trimToNull(props.getProperty(configPrefix + "." + PROP_SPIFFE_VALUE));
+
+ if (ret == null) {
+ String filePath = StringUtils.trimToNull(props.getProperty(configPrefix + "." + PROP_SPIFFE_FILE));
+
+ if (filePath == null) {
+ filePath = DEFAULT_SPIFFE_IDENTITY_FILE;
+ }
+
+ ret = readFirstLine(filePath);
+
+ if (ret == null) {
+ ret = StringUtils.trimToNull(System.getenv(ENV_SPIFFE_ID));
+ }
+ }
+ }
+
+ LOG.debug("resolve(configPrefix={}): ret={}", configPrefix, ret);
+
+
+ return ret;
+ }
+
+ private static String readFirstLine(final String filePath) {
+ String ret = null;
+
+ if (StringUtils.isNotBlank(filePath)) {
+ try {
+ Path path = Paths.get(filePath.trim());
+
+ if (Files.isRegularFile(path)) {
+ List lines = Files.readAllLines(path, StandardCharsets.UTF_8);
+
+ for (String line : lines) {
+ String trimmed = StringUtils.trimToNull(line);
+
+ if (trimmed != null) {
+ ret = trimmed;
+
+ break;
+ }
+ }
+ }
+ } catch (IOException ex) {
+ LOG.debug("Unable to read SPIFFE identity from file {}", filePath, ex);
+ }
+ }
+
+ return ret;
+ }
+}
diff --git a/common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java b/common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java
new file mode 100644
index 00000000000..1e340c914ca
--- /dev/null
+++ b/common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.ranger.plugin.util;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class PluginHeaderAuthConfigTest {
+ private static final String VALID_SPIFFE =
+ "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/om";
+
+ @Test
+ public void buildSpiffeAuthHeadersUsesConfiguredHeaderName() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "true");
+ props.setProperty("ranger.ozone.authn.header.spiffe", "X-Spiffe-Id");
+ props.setProperty("ranger.ozone.authn.spiffe.value", VALID_SPIFFE);
+
+ Map headers = PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, "ranger.ozone");
+
+ assertEquals(VALID_SPIFFE, headers.get("X-Spiffe-Id"));
+ }
+
+ @Test
+ public void buildSpiffeAuthHeadersEmptyWhenDisabled() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "false");
+ props.setProperty("ranger.ozone.authn.spiffe.value", VALID_SPIFFE);
+
+ assertTrue(PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, "ranger.ozone").isEmpty());
+ }
+
+ @Test
+ public void resolveSpiffeIdFromFile(@TempDir Path tempDir) throws Exception {
+ Path spiffeFile = tempDir.resolve("spiffe");
+ Files.writeString(spiffeFile, VALID_SPIFFE + "\n", StandardCharsets.UTF_8);
+
+ Properties props = new Properties();
+ props.setProperty("ranger.hive.authn.spiffe.file", spiffeFile.toString());
+
+ assertEquals(VALID_SPIFFE, SpiffeIdentityResolver.resolve(props, "ranger.hive"));
+ }
+
+ @Test
+ public void isHeaderAuthEnabledFalseForMissingPrefix() {
+ assertFalse(PluginHeaderAuthConfig.isHeaderAuthEnabled(new Properties(), "ranger.ozone"));
+ }
+
+ @Test
+ public void buildSpiffeAuthHeadersEmptyWhenHeaderNamesMisconfigured() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "true");
+ props.setProperty("ranger.ozone.authn.header.spiffe", ",");
+ props.setProperty("ranger.ozone.authn.spiffe.value", VALID_SPIFFE);
+
+ assertTrue(PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, "ranger.ozone").isEmpty());
+ }
+
+ @Test
+ public void buildSpiffeAuthHeadersEmptyWhenSpiffeIdUnresolved() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "true");
+ props.setProperty("ranger.ozone.authn.header.spiffe", "X-Spiffe-Id");
+
+ assertTrue(PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, "ranger.ozone").isEmpty());
+ }
+
+ @Test
+ public void buildSpiffeAuthHeadersEmptyWhenSpiffeIdMalformed() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "true");
+ props.setProperty("ranger.ozone.authn.header.spiffe", "X-Spiffe-Id");
+ props.setProperty("ranger.ozone.authn.spiffe.value", "not-a-spiffe-id");
+
+ assertTrue(PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, "ranger.ozone").isEmpty());
+ }
+}