headers = new LinkedHashMap<>();
+
+ headers.put("X-Spiffe-Id", VALID_SPIFFE);
+ client.setTrustedAuthHeaders(headers);
+
+ try (Response response = client.get("/test", Collections.emptyMap())) {
+ assertEquals(200, response.getStatus());
+ }
+
+ assertEquals(VALID_SPIFFE, capturedSpiffeHeader.get());
+ } finally {
+ httpServer.stop(0);
+ }
+ }
}
diff --git a/common-utils/src/main/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfig.java b/common-utils/src/main/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfig.java
new file mode 100644
index 00000000000..a96a81fa973
--- /dev/null
+++ b/common-utils/src/main/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfig.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.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.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Outbound trusted-header auth for audit-server and other REST clients.
+ *
+ * When {@code authn.header.enabled=true}, trusted HTTP headers are added to every
+ * outbound REST request. Each header is configured as a property whose name is the
+ * HTTP header name under {@code authn.header} (audit destination example):
+ *
+ * xasecure.audit.destination.auditserver.authn.header.enabled=true
+ * xasecure.audit.destination.auditserver.authn.header.X-Spiffe-Id=file:/path/to/spiffe-id.file
+ * # valid value specs:
+ * # file:/path/to/spiffe-id.file
+ * # env:SPIFFE_ID
+ * # spiffe://trust-domain/ns/.../sa/... (literal string)
+ *
+ */
+public final class PluginHeaderAuthConfig {
+ private static final Logger LOG = LoggerFactory.getLogger(PluginHeaderAuthConfig.class);
+
+ public static final String PROP_HEADER_PREFIX = "authn.header.";
+ public static final String PROP_HEADER_AUTH_ENABLED = PROP_HEADER_PREFIX + "enabled";
+
+ private static final String VALUE_PREFIX_FILE = "file:";
+ private static final String VALUE_PREFIX_ENV = "env:";
+
+ private PluginHeaderAuthConfig() {
+ // to block instantiation
+ }
+
+ /**
+ * Returns whether trusted header auth is enabled for the given config prefix.
+ *
+ * @param props plugin or site configuration properties
+ * @param configPrefix property prefix for header-auth settings
+ * @return {@code true} when header auth is enabled
+ */
+ public static boolean isHeaderAuthEnabled(final Properties props, final String configPrefix) {
+ return props != null && StringUtils.isNotBlank(configPrefix) && Boolean.parseBoolean(props.getProperty(configPrefix + "." + PROP_HEADER_AUTH_ENABLED, "false"));
+ }
+
+ /**
+ * Builds trusted HTTP headers for outbound REST calls when header auth is enabled.
+ *
+ * @param props plugin or site configuration properties
+ * @param configPrefix prefix such as {@code xasecure.audit.destination.auditserver}
+ * @return immutable header map; empty when auth is disabled or misconfigured
+ */
+ public static Map buildTrustedAuthHeaders(final Properties props, final String configPrefix) {
+ final Map ret;
+
+ if (isHeaderAuthEnabled(props, configPrefix)) {
+ String propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX;
+ Map headers = new LinkedHashMap<>();
+
+ for (String propertyName : props.stringPropertyNames()) {
+ if (!propertyName.startsWith(propertyPrefix)) {
+ continue;
+ }
+
+ String headerName = propertyName.substring(propertyPrefix.length());
+
+ if (StringUtils.isBlank(headerName) || "enabled".equals(headerName)) {
+ continue;
+ }
+
+ String headerValue = resolveConfiguredValue(props.getProperty(propertyName));
+
+ addConfiguredHeader(headers, configPrefix, headerName, headerValue);
+ }
+
+ if (headers.isEmpty()) {
+ LOG.warn("Plugin header auth enabled for {} but no trusted headers could be resolved", configPrefix);
+ }
+
+ ret = Collections.unmodifiableMap(headers);
+ } else {
+ ret = Collections.emptyMap();
+ }
+
+ return ret;
+ }
+
+ private static void addConfiguredHeader(final Map headers, final String configPrefix, final String headerName, final String headerValue) {
+ if (StringUtils.isBlank(headerValue)) {
+ LOG.warn("Plugin header auth enabled for {} but trusted header {} has no resolvable value", configPrefix, headerName);
+ } else {
+ headers.put(headerName, headerValue);
+ }
+ }
+
+ private static String resolveConfiguredValue(final String valueSpec) {
+ final String ret;
+
+ if (StringUtils.isBlank(valueSpec)) {
+ ret = null;
+ } else if (valueSpec.startsWith(VALUE_PREFIX_FILE)) {
+ ret = readFirstNonBlankLine(valueSpec.substring(VALUE_PREFIX_FILE.length()));
+ } else if (valueSpec.startsWith(VALUE_PREFIX_ENV)) {
+ ret = StringUtils.trimToNull(System.getenv(valueSpec.substring(VALUE_PREFIX_ENV.length())));
+ } else {
+ ret = StringUtils.trimToNull(valueSpec);
+ }
+
+ return ret;
+ }
+
+ private static String readFirstNonBlankLine(final String filePath) {
+ String ret = null;
+
+ if (StringUtils.isNotBlank(filePath)) {
+ try {
+ Path path = Paths.get(filePath.trim());
+
+ if (Files.isRegularFile(path)) {
+ for (String line : Files.readAllLines(path, StandardCharsets.UTF_8)) {
+ String trimmed = StringUtils.trimToNull(line);
+
+ if (trimmed != null) {
+ ret = trimmed;
+
+ break;
+ }
+ }
+ }
+ } catch (IOException ex) {
+ LOG.debug("Unable to read trusted header value 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..98286c39a11
--- /dev/null
+++ b/common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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 buildTrustedAuthHeadersUsesLiteralHeaderValue() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "true");
+ props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", VALID_SPIFFE);
+
+ Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone");
+
+ assertEquals(VALID_SPIFFE, headers.get("X-Spiffe-Id"));
+ }
+
+ @Test
+ public void buildTrustedAuthHeadersUsesFileValueSpec(@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.ozone.authn.header.enabled", "true");
+ props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", "file:" + spiffeFile);
+
+ Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone");
+
+ assertEquals(VALID_SPIFFE, headers.get("X-Spiffe-Id"));
+ }
+
+ @Test
+ public void buildTrustedAuthHeadersEmptyWhenDisabled() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "false");
+ props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", VALID_SPIFFE);
+
+ assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty());
+ }
+
+ @Test
+ public void isHeaderAuthEnabledFalseForMissingPrefix() {
+ assertFalse(PluginHeaderAuthConfig.isHeaderAuthEnabled(new Properties(), "ranger.ozone"));
+ }
+
+ @Test
+ public void buildTrustedAuthHeadersEmptyWhenNoHeadersConfigured() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "true");
+
+ assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty());
+ }
+
+ @Test
+ public void buildTrustedAuthHeadersEmptyWhenSpiffeIdUnresolved() {
+ Properties props = new Properties();
+ props.setProperty("ranger.ozone.authn.header.enabled", "true");
+ props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", "env:UNSET_SPIFFE_ID_VAR");
+
+ assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty());
+ }
+}