From 143fa621dcbdd02571c135eeb142a4121eb52c62 Mon Sep 17 00:00:00 2001 From: ramk Date: Thu, 6 Aug 2026 08:44:07 +0530 Subject: [PATCH 01/16] RANGER-5723: Plugin SPIFFE outbound auth for audit-server destination Add PluginHeaderAuthConfig, SpiffeIdentityResolver, and RangerRESTClient.setTrustedAuthHeaders(); wire SPIFFE headers into RangerAuditServerDestination when audit XML authn.header.enabled=true. --- agents-audit/dest-auditserver/pom.xml | 6 + .../RangerAuditServerDestination.java | 7 + .../RangerAuditServerDestinationTest.java | 57 ++++++ .../ranger/plugin/util/RangerRESTClient.java | 24 +++ .../plugin/util/PluginHeaderAuthConfig.java | 169 ++++++++++++++++++ .../plugin/util/SpiffeIdentityResolver.java | 117 ++++++++++++ .../util/PluginHeaderAuthConfigTest.java | 90 ++++++++++ 7 files changed, 470 insertions(+) create mode 100644 agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java create mode 100644 common-utils/src/main/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfig.java create mode 100644 common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdentityResolver.java create mode 100644 common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java diff --git a/agents-audit/dest-auditserver/pom.xml b/agents-audit/dest-auditserver/pom.xml index 41aa1baee1..d512ebbae9 100644 --- a/agents-audit/dest-auditserver/pom.xml +++ b/agents-audit/dest-auditserver/pom.xml @@ -74,6 +74,12 @@ + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + org.slf4j log4j-over-slf4j diff --git a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java index a7eacb999e..4734e6617d 100644 --- a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java +++ b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java @@ -27,6 +27,7 @@ import org.apache.ranger.audit.model.AuthzAuditEvent; import org.apache.ranger.audit.provider.MiscUtil; import org.apache.ranger.plugin.authn.DefaultJwtProvider; +import org.apache.ranger.plugin.util.PluginHeaderAuthConfig; import org.apache.ranger.plugin.util.RangerRESTClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -98,6 +99,12 @@ public void init(Properties props, String propPrefix) { this.restClient.setMaxRetryAttempts(maxRetryAttempts); this.restClient.setRetryIntervalMs(retryIntervalMs); + Map spiffeHeaders = PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, propPrefix); + if (!spiffeHeaders.isEmpty()) { + this.restClient.setTrustedAuthHeaders(spiffeHeaders); + LOG.debug("SPIFFE header authentication enabled for audit-server destination"); + } + LOG.info("<== RangerAuditServerDestination:init()"); } diff --git a/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java new file mode 100644 index 0000000000..42082f51e4 --- /dev/null +++ b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java @@ -0,0 +1,57 @@ +/* + * 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.audit.destination; + +import org.apache.ranger.plugin.util.PluginHeaderAuthConfig; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RangerAuditServerDestinationTest { + private static final String AUDIT_DEST_PREFIX = "xasecure.audit.destination.auditserver"; + + @Test + public void buildSpiffeAuthHeadersUsesAuditDestinationPrefix() { + Properties props = new Properties(); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "true"); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.spiffe", "X-Spiffe-Id"); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.spiffe.value", + "spiffe://example.com/ns/default/sa/hive"); + + Map headers = PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, AUDIT_DEST_PREFIX); + + assertEquals(1, headers.size()); + assertEquals("spiffe://example.com/ns/default/sa/hive", headers.get("X-Spiffe-Id")); + } + + @Test + public void buildSpiffeAuthHeadersEmptyWhenAuditDestinationDisabled() { + Properties props = new Properties(); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "false"); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.spiffe.value", + "spiffe://example.com/ns/default/sa/hive"); + + Map headers = PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, AUDIT_DEST_PREFIX); + + assertTrue(headers.isEmpty()); + } +} diff --git a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java index d4d49523bd..94046a549e 100644 --- a/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java +++ b/agents-common/src/main/java/org/apache/ranger/plugin/util/RangerRESTClient.java @@ -60,6 +60,8 @@ import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; @@ -143,6 +145,7 @@ public String getMethod() { private volatile Client cookieAuthClient; private JwtProvider jwtProvider; private volatile String authHeader; + private volatile Map trustedAuthHeaders = Collections.emptyMap(); public RangerRESTClient(String url, String sslConfigFileName, Configuration config) { this(url, sslConfigFileName, config, getPropertyPrefix(config)); @@ -215,6 +218,19 @@ public void setRetryIntervalMs(int retryIntervalMs) { this.retryIntervalMs = retryIntervalMs; } + /** + * Trusted HTTP headers for SPIFFE or other header-based auth. + * Applied to every REST request from this client. + */ + public void setTrustedAuthHeaders(Map headers) { + if (headers == null || headers.isEmpty()) { + trustedAuthHeaders = Collections.emptyMap(); + } else { + trustedAuthHeaders = Collections.unmodifiableMap(new LinkedHashMap<>(headers)); + } + resetClient(); + } + public void setBasicAuthInfo(String username, String password) { setBasicAuthFilter(username, password); } @@ -494,9 +510,17 @@ private Invocation.Builder createInvocationBuilder(int currentIndex, String rela builder = builder.cookie(sessionId); } + applyTrustedAuthHeaders(builder); + return builder; } + private void applyTrustedAuthHeaders(Invocation.Builder builder) { + for (Map.Entry entry : trustedAuthHeaders.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } + } + private Response performRequest(HttpMethod method, String relativeUrl, Map params, Object requestBody, Cookie sessionId) throws Exception { Response finalResponse = null; int startIndex = this.lastKnownActiveUrlIndex; 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 0000000000..0f4e77b15e --- /dev/null +++ b/common-utils/src/main/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfig.java @@ -0,0 +1,169 @@ +/* + * 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.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * Outbound trusted-header auth for audit-server and other REST clients. + * + *

Properties are read under a caller-supplied prefix (audit destination example): + *

+ * xasecure.audit.destination.auditserver.authn.header.enabled=true
+ * xasecure.audit.destination.auditserver.authn.header.spiffe=X-Spiffe-Id
+ * 
+ * SPIFFE ID value is resolved via {@link SpiffeIdentityResolver} under the same + * prefix (explicit value, identity file, or {@code SPIFFE_ID} environment variable). + */ +public final class PluginHeaderAuthConfig { + public static final String RANGER_CONFIG_PREFIX = "ranger."; + public static final String PROP_HEADER_AUTH_ENABLED = "authn.header.enabled"; + public static final String PROP_HEADER_SPIFFE = "authn.header.spiffe"; + public static final String DEFAULT_SPIFFE_HEADER_NAME = "X-Spiffe-Id"; + + private static final Logger LOG = + LoggerFactory.getLogger(PluginHeaderAuthConfig.class); + + private PluginHeaderAuthConfig() { + // to block instantiation + } + + /** + * Builds the {@code ranger.} config prefix for a service type. + * + * @param serviceType Ranger service type (e.g. {@code hive}) + * @return the config prefix, or {@code null} when {@code serviceType} is blank + */ + public static String configPrefixForServiceType(final String serviceType) { + if (StringUtils.isBlank(serviceType)) { + return null; + } + + return RANGER_CONFIG_PREFIX + serviceType.trim(); + } + + /** + * Finds the first {@code ranger..authn.header.enabled=true} + * prefix in {@code props}. + * + * @param props plugin or site configuration properties + * @return the matching config prefix, or {@code null} when none is enabled + */ + public static String resolveEnabledConfigPrefix(final Properties props) { + if (props == null || props.isEmpty()) { + return null; + } + + String suffix = "." + PROP_HEADER_AUTH_ENABLED; + + for (String key : props.stringPropertyNames()) { + if (!key.startsWith(RANGER_CONFIG_PREFIX) || !key.endsWith(suffix)) { + continue; + } + + String prefix = key.substring(0, key.length() - suffix.length()); + + if (isHeaderAuthEnabled(props, prefix)) { + return prefix; + } + } + + return null; + } + + /** + * Returns whether trusted header auth is enabled for the given config prefix. + * + * @param props plugin or site configuration properties + * @param configPrefix prefix such as {@code ranger.hive} + * @return {@code true} when header auth is enabled + */ + public static boolean isHeaderAuthEnabled(final Properties props, + final String configPrefix) { + if (props == null || StringUtils.isBlank(configPrefix)) { + return false; + } + + return Boolean.parseBoolean( + props.getProperty(configPrefix + "." + PROP_HEADER_AUTH_ENABLED, + "false")); + } + + /** + * Builds SPIFFE header(s) 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 buildSpiffeAuthHeaders(final Properties props, + final String configPrefix) { + if (!isHeaderAuthEnabled(props, configPrefix)) { + return Collections.emptyMap(); + } + + List headerNames = SpiffeIdUtil.parseHeaderNames( + resolveSpiffeHeaderName(props, configPrefix)); + String spiffeId = SpiffeIdentityResolver.resolve(props, configPrefix); + + if (headerNames.isEmpty()) { + LOG.warn("Plugin header auth enabled for {} but no SPIFFE header " + + "name is configured", configPrefix); + return Collections.emptyMap(); + } + + if (StringUtils.isBlank(spiffeId)) { + LOG.warn("Plugin header auth enabled for {} but no SPIFFE ID could " + + "be resolved", configPrefix); + return Collections.emptyMap(); + } + + if (!SpiffeIdUtil.isValidSpiffeId(spiffeId)) { + LOG.warn("Resolved SPIFFE ID for {} is not well-formed", configPrefix); + return Collections.emptyMap(); + } + + Map headers = new LinkedHashMap<>(); + + for (String headerName : headerNames) { + headers.put(headerName, spiffeId.trim()); + } + + 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 0000000000..1df60d408c --- /dev/null +++ b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdentityResolver.java @@ -0,0 +1,117 @@ +/* + * 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 { + 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 static final Logger LOG = + LoggerFactory.getLogger(SpiffeIdentityResolver.class); + + 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) { + if (props == null || StringUtils.isBlank(configPrefix)) { + return null; + } + + String value = StringUtils.trimToNull( + props.getProperty(configPrefix + "." + PROP_SPIFFE_VALUE)); + + if (value != null) { + return value; + } + + String filePath = StringUtils.trimToNull( + props.getProperty(configPrefix + "." + PROP_SPIFFE_FILE)); + + if (filePath == null) { + filePath = DEFAULT_SPIFFE_IDENTITY_FILE; + } + + value = readFirstLine(filePath); + + if (value != null) { + return value; + } + + return StringUtils.trimToNull(System.getenv(ENV_SPIFFE_ID)); + } + + private static String readFirstLine(final String filePath) { + if (StringUtils.isBlank(filePath)) { + return null; + } + + try { + Path path = Paths.get(filePath.trim()); + + if (!Files.isRegularFile(path)) { + return null; + } + + List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + + for (String line : lines) { + String trimmed = StringUtils.trimToNull(line); + + if (trimmed != null) { + return trimmed; + } + } + } catch (IOException ex) { + LOG.debug("Unable to read SPIFFE identity from file {}", filePath, ex); + } + + return null; + } +} 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 0000000000..b2bd167ac3 --- /dev/null +++ b/common-utils/src/test/java/org/apache/ranger/plugin/util/PluginHeaderAuthConfigTest.java @@ -0,0 +1,90 @@ +/* + * 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.assertNull; +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 resolveEnabledConfigPrefixFindsOzonePrefix() { + Properties props = new Properties(); + props.setProperty("ranger.ozone.authn.header.enabled", "true"); + + assertEquals("ranger.ozone", PluginHeaderAuthConfig.resolveEnabledConfigPrefix(props)); + } + + @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 resolveEnabledConfigPrefixNullWhenDisabled() { + Properties props = new Properties(); + props.setProperty("ranger.hive.authn.header.enabled", "false"); + + assertNull(PluginHeaderAuthConfig.resolveEnabledConfigPrefix(props)); + } +} From dff71f098cbe249e2256cf2c0ad8a41d70aba24b Mon Sep 17 00:00:00 2001 From: ramk Date: Thu, 6 Aug 2026 14:11:48 +0530 Subject: [PATCH 02/16] RANGER-5723: Remove unused SPIFFE prefix-discovery helpers Drop configPrefixForServiceType, resolveEnabledConfigPrefix, and RANGER_CONFIG_PREFIX; audit destination passes an explicit config prefix to buildSpiffeAuthHeaders. --- .../plugin/util/PluginHeaderAuthConfig.java | 48 +------------------ .../util/PluginHeaderAuthConfigTest.java | 17 ------- 2 files changed, 2 insertions(+), 63 deletions(-) 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 index 0f4e77b15e..228d043d4f 100644 --- 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 @@ -5,7 +5,7 @@ * 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 + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -41,7 +41,6 @@ * prefix (explicit value, identity file, or {@code SPIFFE_ID} environment variable). */ public final class PluginHeaderAuthConfig { - public static final String RANGER_CONFIG_PREFIX = "ranger."; public static final String PROP_HEADER_AUTH_ENABLED = "authn.header.enabled"; public static final String PROP_HEADER_SPIFFE = "authn.header.spiffe"; public static final String DEFAULT_SPIFFE_HEADER_NAME = "X-Spiffe-Id"; @@ -53,54 +52,11 @@ private PluginHeaderAuthConfig() { // to block instantiation } - /** - * Builds the {@code ranger.} config prefix for a service type. - * - * @param serviceType Ranger service type (e.g. {@code hive}) - * @return the config prefix, or {@code null} when {@code serviceType} is blank - */ - public static String configPrefixForServiceType(final String serviceType) { - if (StringUtils.isBlank(serviceType)) { - return null; - } - - return RANGER_CONFIG_PREFIX + serviceType.trim(); - } - - /** - * Finds the first {@code ranger..authn.header.enabled=true} - * prefix in {@code props}. - * - * @param props plugin or site configuration properties - * @return the matching config prefix, or {@code null} when none is enabled - */ - public static String resolveEnabledConfigPrefix(final Properties props) { - if (props == null || props.isEmpty()) { - return null; - } - - String suffix = "." + PROP_HEADER_AUTH_ENABLED; - - for (String key : props.stringPropertyNames()) { - if (!key.startsWith(RANGER_CONFIG_PREFIX) || !key.endsWith(suffix)) { - continue; - } - - String prefix = key.substring(0, key.length() - suffix.length()); - - if (isHeaderAuthEnabled(props, prefix)) { - return prefix; - } - } - - return null; - } - /** * Returns whether trusted header auth is enabled for the given config prefix. * * @param props plugin or site configuration properties - * @param configPrefix prefix such as {@code ranger.hive} + * @param configPrefix property prefix for header-auth settings * @return {@code true} when header auth is enabled */ public static boolean isHeaderAuthEnabled(final Properties props, 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 index b2bd167ac3..79578cb5f9 100644 --- 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 @@ -28,21 +28,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; 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 resolveEnabledConfigPrefixFindsOzonePrefix() { - Properties props = new Properties(); - props.setProperty("ranger.ozone.authn.header.enabled", "true"); - - assertEquals("ranger.ozone", PluginHeaderAuthConfig.resolveEnabledConfigPrefix(props)); - } - @Test public void buildSpiffeAuthHeadersUsesConfiguredHeaderName() { Properties props = new Properties(); @@ -79,12 +70,4 @@ public void resolveSpiffeIdFromFile(@TempDir Path tempDir) throws Exception { public void isHeaderAuthEnabledFalseForMissingPrefix() { assertFalse(PluginHeaderAuthConfig.isHeaderAuthEnabled(new Properties(), "ranger.ozone")); } - - @Test - public void resolveEnabledConfigPrefixNullWhenDisabled() { - Properties props = new Properties(); - props.setProperty("ranger.hive.authn.header.enabled", "false"); - - assertNull(PluginHeaderAuthConfig.resolveEnabledConfigPrefix(props)); - } } From 242d31b6f07663c0c5d4311dac8d759f02ff9c8b Mon Sep 17 00:00:00 2001 From: Ramachandran Krishnan Date: Sat, 15 Aug 2026 10:23:50 +0530 Subject: [PATCH 03/16] RANGER-5723: Address PR review comments for SPIFFE outbound auth Remove redundant SPIFFE ID trim, add misconfiguration and REST client header tests, and document that SPIFFE header auth is additive to authn.type. --- .../RangerAuditServerDestination.java | 2 + .../plugin/util/TestRangerRESTClient.java | 49 +++++++++++++++++++ .../plugin/util/PluginHeaderAuthConfig.java | 2 +- .../util/PluginHeaderAuthConfigTest.java | 29 +++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java index 4734e6617d..8bf6c60320 100644 --- a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java +++ b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java @@ -99,6 +99,8 @@ public void init(Properties props, String propPrefix) { this.restClient.setMaxRetryAttempts(maxRetryAttempts); this.restClient.setRetryIntervalMs(retryIntervalMs); + // SPIFFE header auth is orthogonal to authn.type (JWT/Basic/Kerberos): when enabled, + // trusted headers are added in addition to whatever authType configured above. Map spiffeHeaders = PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, propPrefix); if (!spiffeHeaders.isEmpty()) { this.restClient.setTrustedAuthHeaders(spiffeHeaders); diff --git a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java index 9d837a2c4c..465cd26c77 100644 --- a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java +++ b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java @@ -19,11 +19,22 @@ package org.apache.ranger.plugin.util; +import com.sun.net.httpserver.HttpServer; +import org.apache.hadoop.conf.Configuration; import org.apache.ranger.authorization.hadoop.config.RangerPluginConfig; import org.apache.ranger.plugin.policyengine.RangerPolicyEngineOptions; import org.apache.ranger.plugin.service.RangerBasePlugin; import org.junit.jupiter.api.Test; +import javax.ws.rs.core.Response; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -33,6 +44,8 @@ public class TestRangerRESTClient { private static final String SERVICE_NAME = "test-service"; private static final String APP_ID = "test-app"; private static final String ERR_MESSAGE = "Ranger URL is null or empty."; + private static final String VALID_SPIFFE = + "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/om"; @Test public void testPluginInit_WithNoUrl_ThrowsException() { @@ -50,4 +63,40 @@ public void testPluginInit_WithValidUrl_Succeeds() { plugin.init(); assertNotNull(plugin, "RangerBasePlugin should be initialized successfully"); } + + @Test + public void setTrustedAuthHeadersAddsHeaderToOutboundRequest() throws Exception { + AtomicReference capturedSpiffeHeader = new AtomicReference<>(); + HttpServer httpServer = HttpServer.create(new InetSocketAddress(0), 0); + + httpServer.createContext("/", exchange -> { + List values = exchange.getRequestHeaders().get("X-Spiffe-Id"); + + if (values != null && !values.isEmpty()) { + capturedSpiffeHeader.set(values.get(0)); + } + + exchange.sendResponseHeaders(200, -1); + exchange.close(); + }); + httpServer.start(); + + try { + String serverUrl = "http://localhost:" + httpServer.getAddress().getPort(); + Configuration conf = new Configuration(); + RangerRESTClient client = new RangerRESTClient(serverUrl, null, conf); + Map 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 index 228d043d4f..e82326918c 100644 --- 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 @@ -107,7 +107,7 @@ public static Map buildSpiffeAuthHeaders(final Properties props, Map headers = new LinkedHashMap<>(); for (String headerName : headerNames) { - headers.put(headerName, spiffeId.trim()); + headers.put(headerName, spiffeId); } return Collections.unmodifiableMap(headers); 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 index 79578cb5f9..1e340c914c 100644 --- 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 @@ -70,4 +70,33 @@ public void resolveSpiffeIdFromFile(@TempDir Path tempDir) throws Exception { 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()); + } } From fc9ce57208ef8e70098ec917583140ca7aa1061e Mon Sep 17 00:00:00 2001 From: Ramachandran Krishnan Date: Sat, 15 Aug 2026 13:42:59 +0530 Subject: [PATCH 04/16] RANGER-5723: Fix Checkstyle import order in TestRangerRESTClient Add blank line between javax and java import groups required by dev-support/checkstyle.xml ImportOrder rule. --- .../java/org/apache/ranger/plugin/util/TestRangerRESTClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java index 465cd26c77..8a2790cc73 100644 --- a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java +++ b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test; import javax.ws.rs.core.Response; + import java.net.InetSocketAddress; import java.util.Collections; import java.util.LinkedHashMap; From c71543a713deabab7fb7a5b0f7033ce14c3ce8b5 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sat, 15 Aug 2026 11:11:56 -0700 Subject: [PATCH 05/16] Update SpiffeIdentityResolver.java --- .../plugin/util/SpiffeIdentityResolver.java | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) 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 index 1df60d408c..e22f14fc31 100644 --- 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 @@ -39,14 +39,12 @@ * environment variable. */ public final class SpiffeIdentityResolver { - 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 static final Logger LOG = LoggerFactory.getLogger(SpiffeIdentityResolver.class); - 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 @@ -60,58 +58,56 @@ private SpiffeIdentityResolver() { * @return the resolved SPIFFE ID, or {@code null} when unavailable */ public static String resolve(final Properties props, final String configPrefix) { - if (props == null || StringUtils.isBlank(configPrefix)) { - return null; - } - - String value = StringUtils.trimToNull( - props.getProperty(configPrefix + "." + PROP_SPIFFE_VALUE)); - - if (value != null) { - return value; - } + String ret; - String filePath = StringUtils.trimToNull( - props.getProperty(configPrefix + "." + PROP_SPIFFE_FILE)); - - if (filePath == null) { - filePath = DEFAULT_SPIFFE_IDENTITY_FILE; - } - - value = readFirstLine(filePath); - - if (value != null) { - return value; + if (props == null || StringUtils.isBlank(configPrefix)) { + ret = null; + } else { + 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)); + } + } } - return StringUtils.trimToNull(System.getenv(ENV_SPIFFE_ID)); + return ret; } private static String readFirstLine(final String filePath) { - if (StringUtils.isBlank(filePath)) { - return null; - } - - try { - Path path = Paths.get(filePath.trim()); - - if (!Files.isRegularFile(path)) { - return null; - } - - List lines = Files.readAllLines(path, StandardCharsets.UTF_8); - - for (String line : lines) { - String trimmed = StringUtils.trimToNull(line); - - if (trimmed != null) { - return trimmed; + 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); } - } catch (IOException ex) { - LOG.debug("Unable to read SPIFFE identity from file {}", filePath, ex); } - return null; + return ret; } } From 09c69a7ddc38ffe89f60368f87fc1a740942628e Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sat, 15 Aug 2026 11:20:05 -0700 Subject: [PATCH 06/16] Update SpiffeIdentityResolver.java --- .../ranger/plugin/util/SpiffeIdentityResolver.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 index e22f14fc31..8d470d3ced 100644 --- 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 @@ -58,11 +58,9 @@ private SpiffeIdentityResolver() { * @return the resolved SPIFFE ID, or {@code null} when unavailable */ public static String resolve(final Properties props, final String configPrefix) { - String ret; + String ret = null; - if (props == null || StringUtils.isBlank(configPrefix)) { - ret = null; - } else { + if (props != null && StringUtils.isNotBlank(configPrefix)) { ret = StringUtils.trimToNull(props.getProperty(configPrefix + "." + PROP_SPIFFE_VALUE)); if (ret == null) { @@ -80,6 +78,9 @@ public static String resolve(final Properties props, final String configPrefix) } } + LOG.debug("resolve(configPrefix={}): ret={}", configPrefix, ret); + + return ret; } From 42a9d1bdb8c6312c83837f6196024ae178f0203e Mon Sep 17 00:00:00 2001 From: Ramachandran Krishnan Date: Sun, 16 Aug 2026 10:43:59 +0530 Subject: [PATCH 07/16] RANGER-5723: Fix checkstyle and address remaining PR review comments Fix SpiffeIdentityResolver checkstyle violations, rename buildSpiffeAuthHeaders to buildTrustedAuthHeaders, and add generic authn.header.headers slot-based configuration with file:/env: value resolution. --- .../RangerAuditServerDestination.java | 10 +- .../RangerAuditServerDestinationTest.java | 8 +- .../plugin/util/PluginHeaderAuthConfig.java | 180 ++++++++++++++++-- .../plugin/util/SpiffeIdentityResolver.java | 39 ++-- .../util/PluginHeaderAuthConfigTest.java | 36 +++- 5 files changed, 218 insertions(+), 55 deletions(-) diff --git a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java index 8bf6c60320..741666120b 100644 --- a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java +++ b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java @@ -99,12 +99,12 @@ public void init(Properties props, String propPrefix) { this.restClient.setMaxRetryAttempts(maxRetryAttempts); this.restClient.setRetryIntervalMs(retryIntervalMs); - // SPIFFE header auth is orthogonal to authn.type (JWT/Basic/Kerberos): when enabled, + // Trusted header auth is orthogonal to authn.type (JWT/Basic/Kerberos): when enabled, // trusted headers are added in addition to whatever authType configured above. - Map spiffeHeaders = PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, propPrefix); - if (!spiffeHeaders.isEmpty()) { - this.restClient.setTrustedAuthHeaders(spiffeHeaders); - LOG.debug("SPIFFE header authentication enabled for audit-server destination"); + Map trustedHeaders = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, propPrefix); + if (!trustedHeaders.isEmpty()) { + this.restClient.setTrustedAuthHeaders(trustedHeaders); + LOG.debug("Trusted header authentication enabled for audit-server destination"); } LOG.info("<== RangerAuditServerDestination:init()"); diff --git a/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java index 42082f51e4..102b2289f9 100644 --- a/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java +++ b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java @@ -30,27 +30,27 @@ public class RangerAuditServerDestinationTest { private static final String AUDIT_DEST_PREFIX = "xasecure.audit.destination.auditserver"; @Test - public void buildSpiffeAuthHeadersUsesAuditDestinationPrefix() { + public void buildTrustedAuthHeadersUsesAuditDestinationPrefix() { Properties props = new Properties(); props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "true"); props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.spiffe", "X-Spiffe-Id"); props.setProperty(AUDIT_DEST_PREFIX + ".authn.spiffe.value", "spiffe://example.com/ns/default/sa/hive"); - Map headers = PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, AUDIT_DEST_PREFIX); + Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX); assertEquals(1, headers.size()); assertEquals("spiffe://example.com/ns/default/sa/hive", headers.get("X-Spiffe-Id")); } @Test - public void buildSpiffeAuthHeadersEmptyWhenAuditDestinationDisabled() { + public void buildTrustedAuthHeadersEmptyWhenAuditDestinationDisabled() { Properties props = new Properties(); props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "false"); props.setProperty(AUDIT_DEST_PREFIX + ".authn.spiffe.value", "spiffe://example.com/ns/default/sa/hive"); - Map headers = PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, AUDIT_DEST_PREFIX); + Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX); assertTrue(headers.isEmpty()); } 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 index e82326918c..acefddd666 100644 --- 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 @@ -4,7 +4,7 @@ * 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 + * "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 @@ -23,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -32,19 +33,35 @@ /** * Outbound trusted-header auth for audit-server and other REST clients. * - *

Properties are read under a caller-supplied prefix (audit destination example): + *

Legacy SPIFFE configuration (audit destination example): *

  * xasecure.audit.destination.auditserver.authn.header.enabled=true
  * xasecure.audit.destination.auditserver.authn.header.spiffe=X-Spiffe-Id
+ * xasecure.audit.destination.auditserver.authn.spiffe.value=spiffe://...
  * 
- * SPIFFE ID value is resolved via {@link SpiffeIdentityResolver} under the same - * prefix (explicit value, identity file, or {@code SPIFFE_ID} environment variable). + * + *

Generic slot-based configuration: + *

+ * authn.header.enabled=true
+ * authn.header.headers=spiffe,value
+ * authn.header.spiffe=X-Spiffe-Id
+ * authn.header.value=file:/path/to/spiffe-id.file
+ * 
+ * Value specs support {@code file:}, {@code env:}, or a literal string. Multiple + * headers can be configured with {@code authn.header.{slot}} for the HTTP header + * name and {@code authn.header.{slot}.value} for the value spec. */ public final class PluginHeaderAuthConfig { public static final String PROP_HEADER_AUTH_ENABLED = "authn.header.enabled"; + public static final String PROP_HEADER_HEADERS = "authn.header.headers"; public static final String PROP_HEADER_SPIFFE = "authn.header.spiffe"; public static final String DEFAULT_SPIFFE_HEADER_NAME = "X-Spiffe-Id"; + private static final String SLOT_SPIFFE = "spiffe"; + private static final String SLOT_VALUE = "value"; + private static final String VALUE_PREFIX_FILE = "file:"; + private static final String VALUE_PREFIX_ENV = "env:"; + private static final Logger LOG = LoggerFactory.getLogger(PluginHeaderAuthConfig.class); @@ -71,54 +88,181 @@ public static boolean isHeaderAuthEnabled(final Properties props, } /** - * Builds SPIFFE header(s) for outbound REST calls when header auth is enabled. + * 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 buildSpiffeAuthHeaders(final Properties props, + public static Map buildTrustedAuthHeaders(final Properties props, final String configPrefix) { if (!isHeaderAuthEnabled(props, configPrefix)) { return Collections.emptyMap(); } + String headersConfig = getProperty(props, configPrefix, PROP_HEADER_HEADERS); + + if (StringUtils.isBlank(headersConfig)) { + return buildLegacyTrustedHeaders(props, configPrefix); + } + + return buildConfiguredTrustedHeaders(props, configPrefix, headersConfig); + } + + private static Map buildLegacyTrustedHeaders(final Properties props, + final String configPrefix) { List headerNames = SpiffeIdUtil.parseHeaderNames( resolveSpiffeHeaderName(props, configPrefix)); - String spiffeId = SpiffeIdentityResolver.resolve(props, configPrefix); + String headerValue = SpiffeIdentityResolver.resolve(props, configPrefix); + + return buildSingleTrustedHeader(configPrefix, headerNames, headerValue); + } + + private static Map buildConfiguredTrustedHeaders(final Properties props, + final String configPrefix, final String headersConfig) { + List slots = parseHeaderSlots(headersConfig); + + if (slots.isEmpty()) { + LOG.warn("Plugin header auth enabled for {} but {} is empty", + configPrefix, PROP_HEADER_HEADERS); + return Collections.emptyMap(); + } + + Map headers = new LinkedHashMap<>(); + + if (slots.contains(SLOT_SPIFFE) && slots.contains(SLOT_VALUE)) { + addConfiguredHeader(headers, configPrefix, + getSlotConfig(props, configPrefix, SLOT_SPIFFE), + resolveConfiguredValue(getSlotConfig(props, configPrefix, SLOT_VALUE), + props, configPrefix)); + } else { + for (String slot : slots) { + if (SLOT_VALUE.equals(slot)) { + continue; + } + + String headerName = getSlotConfig(props, configPrefix, slot); + String valueSpec = getSlotConfig(props, configPrefix, slot + ".value"); + + if (valueSpec == null) { + valueSpec = getSlotConfig(props, configPrefix, SLOT_VALUE); + } + + addConfiguredHeader(headers, configPrefix, headerName, + resolveConfiguredValue(valueSpec, props, configPrefix)); + } + } + + return Collections.unmodifiableMap(headers); + } + private static Map buildSingleTrustedHeader(final String configPrefix, + final List headerNames, final String headerValue) { if (headerNames.isEmpty()) { - LOG.warn("Plugin header auth enabled for {} but no SPIFFE header " + LOG.warn("Plugin header auth enabled for {} but no trusted header " + "name is configured", configPrefix); return Collections.emptyMap(); } - if (StringUtils.isBlank(spiffeId)) { - LOG.warn("Plugin header auth enabled for {} but no SPIFFE ID could " - + "be resolved", configPrefix); + if (StringUtils.isBlank(headerValue)) { + LOG.warn("Plugin header auth enabled for {} but no trusted header " + + "value could be resolved", configPrefix); return Collections.emptyMap(); } - if (!SpiffeIdUtil.isValidSpiffeId(spiffeId)) { - LOG.warn("Resolved SPIFFE ID for {} is not well-formed", configPrefix); + if (!SpiffeIdUtil.isValidSpiffeId(headerValue)) { + LOG.warn("Resolved trusted header value for {} is not a well-formed " + + "SPIFFE ID", configPrefix); return Collections.emptyMap(); } Map headers = new LinkedHashMap<>(); for (String headerName : headerNames) { - headers.put(headerName, spiffeId); + headers.put(headerName, headerValue); } return Collections.unmodifiableMap(headers); } + private static void addConfiguredHeader(final Map headers, + final String configPrefix, final String headerName, + final String headerValue) { + if (StringUtils.isBlank(headerName)) { + LOG.warn("Plugin header auth enabled for {} but a trusted header " + + "name slot is not configured", configPrefix); + return; + } + + if (StringUtils.isBlank(headerValue)) { + LOG.warn("Plugin header auth enabled for {} but trusted header {} " + + "has no resolvable value", configPrefix, headerName); + return; + } + + if (SpiffeIdUtil.isValidSpiffeId(headerValue) + || headerValue.startsWith("spiffe://")) { + if (!SpiffeIdUtil.isValidSpiffeId(headerValue)) { + LOG.warn("Resolved trusted header value for {} is not a " + + "well-formed SPIFFE ID", configPrefix); + return; + } + } + + headers.put(headerName, headerValue); + } + + private static String resolveConfiguredValue(final String valueSpec, + final Properties props, final String configPrefix) { + String ret = null; + + if (StringUtils.isBlank(valueSpec)) { + ret = SpiffeIdentityResolver.resolve(props, configPrefix); + } else if (valueSpec.startsWith(VALUE_PREFIX_FILE)) { + ret = SpiffeIdentityResolver.readFirstLine( + 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 List parseHeaderSlots(final String headersConfig) { + List ret = new ArrayList<>(); + + for (String slot : headersConfig.split(",")) { + String trimmed = StringUtils.trimToNull(slot); + + if (trimmed != null) { + ret.add(trimmed); + } + } + + return ret; + } + + private static String getSlotConfig(final Properties props, + final String configPrefix, final String slot) { + return getProperty(props, configPrefix, "authn.header." + slot); + } + + private static String getProperty(final Properties props, + final String configPrefix, final String propertySuffix) { + if (props == null || StringUtils.isBlank(configPrefix)) { + return null; + } + + return StringUtils.trimToNull( + props.getProperty(configPrefix + "." + propertySuffix)); + } + private static String resolveSpiffeHeaderName(final Properties props, final String configPrefix) { - String headerName = props != null - ? StringUtils.trimToNull( - props.getProperty(configPrefix + "." + PROP_HEADER_SPIFFE)) - : null; + String headerName = getProperty(props, configPrefix, PROP_HEADER_SPIFFE); 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 index 8d470d3ced..0cbc342d32 100644 --- 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 @@ -4,8 +4,8 @@ * 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 + * "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 * @@ -39,12 +39,14 @@ * 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"; - 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 static final Logger LOG = + LoggerFactory.getLogger(SpiffeIdentityResolver.class); private SpiffeIdentityResolver() { // to block instantiation @@ -61,17 +63,19 @@ 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)); - + ret = StringUtils.trimToNull( + props.getProperty(configPrefix + "." + PROP_SPIFFE_VALUE)); + if (ret == null) { - String filePath = StringUtils.trimToNull(props.getProperty(configPrefix + "." + PROP_SPIFFE_FILE)); - + 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)); } @@ -80,23 +84,22 @@ public static String resolve(final Properties props, final String configPrefix) LOG.debug("resolve(configPrefix={}): ret={}", configPrefix, ret); - return ret; } - private static String readFirstLine(final String filePath) { + 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; 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 index 1e340c914c..36bae7731a 100644 --- 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 @@ -35,24 +35,40 @@ public class PluginHeaderAuthConfigTest { "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/om"; @Test - public void buildSpiffeAuthHeadersUsesConfiguredHeaderName() { + public void buildTrustedAuthHeadersUsesConfiguredHeaderName() { 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"); + Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone"); assertEquals(VALID_SPIFFE, headers.get("X-Spiffe-Id")); } @Test - public void buildSpiffeAuthHeadersEmptyWhenDisabled() { + public void buildTrustedAuthHeadersUsesConfiguredHeaderSlots(@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.headers", "spiffe,value"); + props.setProperty("ranger.ozone.authn.header.spiffe", "X-Spiffe-Id"); + props.setProperty("ranger.ozone.authn.header.value", "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.spiffe.value", VALID_SPIFFE); - assertTrue(PluginHeaderAuthConfig.buildSpiffeAuthHeaders(props, "ranger.ozone").isEmpty()); + assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } @Test @@ -72,31 +88,31 @@ public void isHeaderAuthEnabledFalseForMissingPrefix() { } @Test - public void buildSpiffeAuthHeadersEmptyWhenHeaderNamesMisconfigured() { + public void buildTrustedAuthHeadersEmptyWhenHeaderNamesMisconfigured() { 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()); + assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } @Test - public void buildSpiffeAuthHeadersEmptyWhenSpiffeIdUnresolved() { + public void buildTrustedAuthHeadersEmptyWhenSpiffeIdUnresolved() { 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()); + assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } @Test - public void buildSpiffeAuthHeadersEmptyWhenSpiffeIdMalformed() { + public void buildTrustedAuthHeadersEmptyWhenSpiffeIdMalformed() { 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()); + assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } } From fa2b61a5b81a309f2d19edb88178fc42f4a3ce04 Mon Sep 17 00:00:00 2001 From: Ramachandran Krishnan Date: Sun, 16 Aug 2026 10:49:37 +0530 Subject: [PATCH 08/16] RANGER-5723: Document trusted-header config modes and value specs Expand PluginHeaderAuthConfig Javadoc for legacy SPIFFE and generic slot configuration, including file:/env:/literal value spec examples. --- .../plugin/util/PluginHeaderAuthConfig.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) 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 index acefddd666..afd822fb6f 100644 --- 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 @@ -33,23 +33,35 @@ /** * Outbound trusted-header auth for audit-server and other REST clients. * - *

Legacy SPIFFE configuration (audit destination example): + *

When {@code authn.header.enabled=true}, trusted HTTP headers are added to every + * outbound REST request. Header names and values are resolved from configuration under + * a caller-supplied prefix (audit destination example below). + * + *

Legacy mode (no {@code authn.header.headers}): a single SPIFFE workload-identity + * header is built from {@code authn.header.spiffe} (HTTP header name, default + * {@code X-Spiffe-Id}) and a SPIFFE ID resolved by {@link SpiffeIdentityResolver} + * ({@code authn.spiffe.value}, {@code authn.spiffe.file}, or {@code SPIFFE_ID} env). *

  * xasecure.audit.destination.auditserver.authn.header.enabled=true
  * xasecure.audit.destination.auditserver.authn.header.spiffe=X-Spiffe-Id
  * xasecure.audit.destination.auditserver.authn.spiffe.value=spiffe://...
  * 
* - *

Generic slot-based configuration: + *

Generic slot-based mode ({@code authn.header.headers} set): header names and + * values are read from {@code authn.header.{slot}} properties. Value specs support + * {@code file:}, {@code env:}, or a literal string. *

- * authn.header.enabled=true
- * authn.header.headers=spiffe,value
- * authn.header.spiffe=X-Spiffe-Id
- * authn.header.value=file:/path/to/spiffe-id.file
+ * xasecure.audit.destination.auditserver.authn.header.enabled=true
+ * xasecure.audit.destination.auditserver.authn.header.headers=spiffe,value
+ * xasecure.audit.destination.auditserver.authn.header.spiffe=X-Spiffe-Id
+ * xasecure.audit.destination.auditserver.authn.header.value=file:/path/to/spiffe-id.file
+ * # valid value specs for authn.header.{slot}.value (or the "value" slot):
+ * #   file:/path/to/spiffe-id.file
+ * #   env:SPIFFE_ID
+ * #   spiffe://trust-domain/ns/.../sa/...  (literal string)
  * 
- * Value specs support {@code file:}, {@code env:}, or a literal string. Multiple - * headers can be configured with {@code authn.header.{slot}} for the HTTP header - * name and {@code authn.header.{slot}.value} for the value spec. + * Multiple headers can also be configured with {@code authn.header.{slot}} for the + * HTTP header name and {@code authn.header.{slot}.value} for the value spec. */ public final class PluginHeaderAuthConfig { public static final String PROP_HEADER_AUTH_ENABLED = "authn.header.enabled"; @@ -90,6 +102,9 @@ public static boolean isHeaderAuthEnabled(final Properties props, /** * Builds trusted HTTP headers for outbound REST calls when header auth is enabled. * + *

Uses legacy SPIFFE resolution when {@code authn.header.headers} is unset; + * otherwise resolves headers from configured slots (see class Javadoc). + * * @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 From 23f4e512df3c1b4d35234aebf07a572322dd4fb7 Mon Sep 17 00:00:00 2001 From: ramk Date: Sun, 16 Aug 2026 22:52:44 +0530 Subject: [PATCH 09/16] RANGER-5723: Use header-name-as-property for trusted auth config Address PR review: configure outbound trusted headers as authn.header.{Header-Name}=value specs (file:/env:/literal) instead of slot-based or legacy SPIFFE properties. --- .../RangerAuditServerDestinationTest.java | 11 +- .../plugin/util/PluginHeaderAuthConfig.java | 181 ++++-------------- .../util/PluginHeaderAuthConfigTest.java | 22 +-- 3 files changed, 45 insertions(+), 169 deletions(-) diff --git a/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java index 102b2289f9..fffc91f798 100644 --- a/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java +++ b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java @@ -33,22 +33,21 @@ public class RangerAuditServerDestinationTest { public void buildTrustedAuthHeadersUsesAuditDestinationPrefix() { Properties props = new Properties(); props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "true"); - props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.spiffe", "X-Spiffe-Id"); - props.setProperty(AUDIT_DEST_PREFIX + ".authn.spiffe.value", - "spiffe://example.com/ns/default/sa/hive"); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", + "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive"); Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX); assertEquals(1, headers.size()); - assertEquals("spiffe://example.com/ns/default/sa/hive", headers.get("X-Spiffe-Id")); + assertEquals("spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive", headers.get("X-Spiffe-Id")); } @Test public void buildTrustedAuthHeadersEmptyWhenAuditDestinationDisabled() { Properties props = new Properties(); props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "false"); - props.setProperty(AUDIT_DEST_PREFIX + ".authn.spiffe.value", - "spiffe://example.com/ns/default/sa/hive"); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", + "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive"); Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX); 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 index afd822fb6f..ca0f8d7039 100644 --- 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 @@ -34,43 +34,21 @@ * 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. Header names and values are resolved from configuration under - * a caller-supplied prefix (audit destination example below). - * - *

Legacy mode (no {@code authn.header.headers}): a single SPIFFE workload-identity - * header is built from {@code authn.header.spiffe} (HTTP header name, default - * {@code X-Spiffe-Id}) and a SPIFFE ID resolved by {@link SpiffeIdentityResolver} - * ({@code authn.spiffe.value}, {@code authn.spiffe.file}, or {@code SPIFFE_ID} env). + * 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.spiffe=X-Spiffe-Id
- * xasecure.audit.destination.auditserver.authn.spiffe.value=spiffe://...
- * 
- * - *

Generic slot-based mode ({@code authn.header.headers} set): header names and - * values are read from {@code authn.header.{slot}} properties. Value specs support - * {@code file:}, {@code env:}, or a literal string. - *

- * xasecure.audit.destination.auditserver.authn.header.enabled=true
- * xasecure.audit.destination.auditserver.authn.header.headers=spiffe,value
- * xasecure.audit.destination.auditserver.authn.header.spiffe=X-Spiffe-Id
- * xasecure.audit.destination.auditserver.authn.header.value=file:/path/to/spiffe-id.file
- * # valid value specs for authn.header.{slot}.value (or the "value" slot):
+ * 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)
  * 
- * Multiple headers can also be configured with {@code authn.header.{slot}} for the - * HTTP header name and {@code authn.header.{slot}.value} for the value spec. */ public final class PluginHeaderAuthConfig { - public static final String PROP_HEADER_AUTH_ENABLED = "authn.header.enabled"; - public static final String PROP_HEADER_HEADERS = "authn.header.headers"; - public static final String PROP_HEADER_SPIFFE = "authn.header.spiffe"; - public static final String DEFAULT_SPIFFE_HEADER_NAME = "X-Spiffe-Id"; + public static final String PROP_HEADER_AUTH_ENABLED = "authn.header.enabled"; + public static final String PROP_HEADER_PREFIX = "authn.header."; - private static final String SLOT_SPIFFE = "spiffe"; - private static final String SLOT_VALUE = "value"; private static final String VALUE_PREFIX_FILE = "file:"; private static final String VALUE_PREFIX_ENV = "env:"; @@ -102,9 +80,6 @@ public static boolean isHeaderAuthEnabled(final Properties props, /** * Builds trusted HTTP headers for outbound REST calls when header auth is enabled. * - *

Uses legacy SPIFFE resolution when {@code authn.header.headers} is unset; - * otherwise resolves headers from configured slots (see class Javadoc). - * * @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 @@ -115,86 +90,28 @@ public static Map buildTrustedAuthHeaders(final Properties props return Collections.emptyMap(); } - String headersConfig = getProperty(props, configPrefix, PROP_HEADER_HEADERS); - - if (StringUtils.isBlank(headersConfig)) { - return buildLegacyTrustedHeaders(props, configPrefix); - } - - return buildConfiguredTrustedHeaders(props, configPrefix, headersConfig); - } - - private static Map buildLegacyTrustedHeaders(final Properties props, - final String configPrefix) { - List headerNames = SpiffeIdUtil.parseHeaderNames( - resolveSpiffeHeaderName(props, configPrefix)); - String headerValue = SpiffeIdentityResolver.resolve(props, configPrefix); - - return buildSingleTrustedHeader(configPrefix, headerNames, headerValue); - } - - private static Map buildConfiguredTrustedHeaders(final Properties props, - final String configPrefix, final String headersConfig) { - List slots = parseHeaderSlots(headersConfig); - - if (slots.isEmpty()) { - LOG.warn("Plugin header auth enabled for {} but {} is empty", - configPrefix, PROP_HEADER_HEADERS); - return Collections.emptyMap(); - } - + String propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX; Map headers = new LinkedHashMap<>(); - if (slots.contains(SLOT_SPIFFE) && slots.contains(SLOT_VALUE)) { - addConfiguredHeader(headers, configPrefix, - getSlotConfig(props, configPrefix, SLOT_SPIFFE), - resolveConfiguredValue(getSlotConfig(props, configPrefix, SLOT_VALUE), - props, configPrefix)); - } else { - for (String slot : slots) { - if (SLOT_VALUE.equals(slot)) { - continue; - } - - String headerName = getSlotConfig(props, configPrefix, slot); - String valueSpec = getSlotConfig(props, configPrefix, slot + ".value"); - - if (valueSpec == null) { - valueSpec = getSlotConfig(props, configPrefix, SLOT_VALUE); - } - - addConfiguredHeader(headers, configPrefix, headerName, - resolveConfiguredValue(valueSpec, props, configPrefix)); + for (String propertyName : sortedPropertyNames(props)) { + if (!propertyName.startsWith(propertyPrefix)) { + continue; } - } - return Collections.unmodifiableMap(headers); - } + String headerName = propertyName.substring(propertyPrefix.length()); - private static Map buildSingleTrustedHeader(final String configPrefix, - final List headerNames, final String headerValue) { - if (headerNames.isEmpty()) { - LOG.warn("Plugin header auth enabled for {} but no trusted header " - + "name is configured", configPrefix); - return Collections.emptyMap(); - } + if (StringUtils.isBlank(headerName) || "enabled".equals(headerName)) { + continue; + } - if (StringUtils.isBlank(headerValue)) { - LOG.warn("Plugin header auth enabled for {} but no trusted header " - + "value could be resolved", configPrefix); - return Collections.emptyMap(); - } + String headerValue = resolveConfiguredValue(props.getProperty(propertyName)); - if (!SpiffeIdUtil.isValidSpiffeId(headerValue)) { - LOG.warn("Resolved trusted header value for {} is not a well-formed " - + "SPIFFE ID", configPrefix); - return Collections.emptyMap(); + addConfiguredHeader(headers, configPrefix, headerName, headerValue); } - Map headers = new LinkedHashMap<>(); - - for (String headerName : headerNames) { - headers.put(headerName, headerValue); + if (headers.isEmpty()) { + LOG.warn("Plugin header auth enabled for {} but no trusted headers " + + "could be resolved", configPrefix); } return Collections.unmodifiableMap(headers); @@ -203,36 +120,27 @@ private static Map buildSingleTrustedHeader(final String configP private static void addConfiguredHeader(final Map headers, final String configPrefix, final String headerName, final String headerValue) { - if (StringUtils.isBlank(headerName)) { - LOG.warn("Plugin header auth enabled for {} but a trusted header " - + "name slot is not configured", configPrefix); - return; - } - if (StringUtils.isBlank(headerValue)) { LOG.warn("Plugin header auth enabled for {} but trusted header {} " + "has no resolvable value", configPrefix, headerName); return; } - if (SpiffeIdUtil.isValidSpiffeId(headerValue) - || headerValue.startsWith("spiffe://")) { - if (!SpiffeIdUtil.isValidSpiffeId(headerValue)) { - LOG.warn("Resolved trusted header value for {} is not a " - + "well-formed SPIFFE ID", configPrefix); - return; - } + if (headerValue.startsWith("spiffe://") + && !SpiffeIdUtil.isValidSpiffeId(headerValue)) { + LOG.warn("Resolved trusted header value for {} is not a " + + "well-formed SPIFFE ID", configPrefix); + return; } headers.put(headerName, headerValue); } - private static String resolveConfiguredValue(final String valueSpec, - final Properties props, final String configPrefix) { + private static String resolveConfiguredValue(final String valueSpec) { String ret = null; if (StringUtils.isBlank(valueSpec)) { - ret = SpiffeIdentityResolver.resolve(props, configPrefix); + ret = null; } else if (valueSpec.startsWith(VALUE_PREFIX_FILE)) { ret = SpiffeIdentityResolver.readFirstLine( valueSpec.substring(VALUE_PREFIX_FILE.length())); @@ -246,39 +154,14 @@ private static String resolveConfiguredValue(final String valueSpec, return ret; } - private static List parseHeaderSlots(final String headersConfig) { - List ret = new ArrayList<>(); - - for (String slot : headersConfig.split(",")) { - String trimmed = StringUtils.trimToNull(slot); + private static List sortedPropertyNames(final Properties props) { + List names = new ArrayList<>(); - if (trimmed != null) { - ret.add(trimmed); - } + if (props != null) { + names.addAll(props.stringPropertyNames()); + Collections.sort(names); } - return ret; - } - - private static String getSlotConfig(final Properties props, - final String configPrefix, final String slot) { - return getProperty(props, configPrefix, "authn.header." + slot); - } - - private static String getProperty(final Properties props, - final String configPrefix, final String propertySuffix) { - if (props == null || StringUtils.isBlank(configPrefix)) { - return null; - } - - return StringUtils.trimToNull( - props.getProperty(configPrefix + "." + propertySuffix)); - } - - private static String resolveSpiffeHeaderName(final Properties props, - final String configPrefix) { - String headerName = getProperty(props, configPrefix, PROP_HEADER_SPIFFE); - - return headerName != null ? headerName : DEFAULT_SPIFFE_HEADER_NAME; + return names; } } 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 index 36bae7731a..c8bfd139ba 100644 --- 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 @@ -35,11 +35,10 @@ public class PluginHeaderAuthConfigTest { "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/om"; @Test - public void buildTrustedAuthHeadersUsesConfiguredHeaderName() { + public void buildTrustedAuthHeadersUsesLiteralHeaderValue() { 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); + props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", VALID_SPIFFE); Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone"); @@ -47,15 +46,13 @@ public void buildTrustedAuthHeadersUsesConfiguredHeaderName() { } @Test - public void buildTrustedAuthHeadersUsesConfiguredHeaderSlots(@TempDir Path tempDir) throws Exception { + 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.headers", "spiffe,value"); - props.setProperty("ranger.ozone.authn.header.spiffe", "X-Spiffe-Id"); - props.setProperty("ranger.ozone.authn.header.value", "file:" + spiffeFile); + props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", "file:" + spiffeFile); Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone"); @@ -66,7 +63,7 @@ public void buildTrustedAuthHeadersUsesConfiguredHeaderSlots(@TempDir Path tempD public void buildTrustedAuthHeadersEmptyWhenDisabled() { Properties props = new Properties(); props.setProperty("ranger.ozone.authn.header.enabled", "false"); - props.setProperty("ranger.ozone.authn.spiffe.value", VALID_SPIFFE); + props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", VALID_SPIFFE); assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } @@ -88,11 +85,9 @@ public void isHeaderAuthEnabledFalseForMissingPrefix() { } @Test - public void buildTrustedAuthHeadersEmptyWhenHeaderNamesMisconfigured() { + public void buildTrustedAuthHeadersEmptyWhenNoHeadersConfigured() { 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.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } @@ -101,7 +96,7 @@ public void buildTrustedAuthHeadersEmptyWhenHeaderNamesMisconfigured() { public void buildTrustedAuthHeadersEmptyWhenSpiffeIdUnresolved() { 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.header.X-Spiffe-Id", "env:UNSET_SPIFFE_ID_VAR"); assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } @@ -110,8 +105,7 @@ public void buildTrustedAuthHeadersEmptyWhenSpiffeIdUnresolved() { public void buildTrustedAuthHeadersEmptyWhenSpiffeIdMalformed() { 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"); + props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", "spiffe://not-valid"); assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } From 6dc549c93a4df9615121801837eab854992b77c0 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 16 Aug 2026 10:35:17 -0700 Subject: [PATCH 10/16] Update RangerAuditServerDestination.java --- .../audit/destination/RangerAuditServerDestination.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java index 741666120b..421ac0710b 100644 --- a/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java +++ b/agents-audit/dest-auditserver/src/main/java/org/apache/ranger/audit/destination/RangerAuditServerDestination.java @@ -102,9 +102,11 @@ public void init(Properties props, String propPrefix) { // Trusted header auth is orthogonal to authn.type (JWT/Basic/Kerberos): when enabled, // trusted headers are added in addition to whatever authType configured above. Map trustedHeaders = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, propPrefix); + if (!trustedHeaders.isEmpty()) { this.restClient.setTrustedAuthHeaders(trustedHeaders); - LOG.debug("Trusted header authentication enabled for audit-server destination"); + + LOG.debug("Trusted authentication headers added for audit-server destination"); } LOG.info("<== RangerAuditServerDestination:init()"); From a25db2a590d7e05df66c72abe2275a0f2af93e2d Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 16 Aug 2026 10:36:34 -0700 Subject: [PATCH 11/16] Update RangerAuditServerDestinationTest.java --- .../destination/RangerAuditServerDestinationTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java index fffc91f798..4ba059e3f3 100644 --- a/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java +++ b/agents-audit/dest-auditserver/src/test/java/org/apache/ranger/audit/destination/RangerAuditServerDestinationTest.java @@ -32,9 +32,9 @@ public class RangerAuditServerDestinationTest { @Test public void buildTrustedAuthHeadersUsesAuditDestinationPrefix() { Properties props = new Properties(); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "true"); - props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", - "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive"); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive"); Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX); @@ -45,9 +45,9 @@ public void buildTrustedAuthHeadersUsesAuditDestinationPrefix() { @Test public void buildTrustedAuthHeadersEmptyWhenAuditDestinationDisabled() { Properties props = new Properties(); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.enabled", "false"); - props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", - "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive"); + props.setProperty(AUDIT_DEST_PREFIX + ".authn.header.X-Spiffe-Id", "spiffe://prod-cluster.k8s.example.com/ns/ranger/sa/hive"); Map headers = PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, AUDIT_DEST_PREFIX); From a49c0d48a704367b989df1cebea1c7a342cb942e Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 16 Aug 2026 10:39:31 -0700 Subject: [PATCH 12/16] Update TestRangerRESTClient.java --- .../ranger/plugin/util/TestRangerRESTClient.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java index 8a2790cc73..2f5f6ca221 100644 --- a/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java +++ b/agents-common/src/test/java/org/apache/ranger/plugin/util/TestRangerRESTClient.java @@ -68,7 +68,7 @@ public void testPluginInit_WithValidUrl_Succeeds() { @Test public void setTrustedAuthHeadersAddsHeaderToOutboundRequest() throws Exception { AtomicReference capturedSpiffeHeader = new AtomicReference<>(); - HttpServer httpServer = HttpServer.create(new InetSocketAddress(0), 0); + HttpServer httpServer = HttpServer.create(new InetSocketAddress(0), 0); httpServer.createContext("/", exchange -> { List values = exchange.getRequestHeaders().get("X-Spiffe-Id"); @@ -80,13 +80,14 @@ public void setTrustedAuthHeadersAddsHeaderToOutboundRequest() throws Exception exchange.sendResponseHeaders(200, -1); exchange.close(); }); + httpServer.start(); try { - String serverUrl = "http://localhost:" + httpServer.getAddress().getPort(); - Configuration conf = new Configuration(); - RangerRESTClient client = new RangerRESTClient(serverUrl, null, conf); - Map headers = new LinkedHashMap<>(); + String serverUrl = "http://localhost:" + httpServer.getAddress().getPort(); + Configuration conf = new Configuration(); + RangerRESTClient client = new RangerRESTClient(serverUrl, null, conf); + Map headers = new LinkedHashMap<>(); headers.put("X-Spiffe-Id", VALID_SPIFFE); client.setTrustedAuthHeaders(headers); From 74afba108854846254b2d39f6844e169097c8083 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 16 Aug 2026 10:49:40 -0700 Subject: [PATCH 13/16] Update PluginHeaderAuthConfig.java --- .../plugin/util/PluginHeaderAuthConfig.java | 57 ++++++------------- 1 file changed, 18 insertions(+), 39 deletions(-) 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 index ca0f8d7039..8b1546d0d7 100644 --- 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 @@ -46,15 +46,14 @@ * */ public final class PluginHeaderAuthConfig { - public static final String PROP_HEADER_AUTH_ENABLED = "authn.header.enabled"; + 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 static final Logger LOG = - LoggerFactory.getLogger(PluginHeaderAuthConfig.class); - private PluginHeaderAuthConfig() { // to block instantiation } @@ -66,15 +65,8 @@ private PluginHeaderAuthConfig() { * @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) { - if (props == null || StringUtils.isBlank(configPrefix)) { - return false; - } - - return Boolean.parseBoolean( - props.getProperty(configPrefix + "." + PROP_HEADER_AUTH_ENABLED, - "false")); + 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")); } /** @@ -84,14 +76,13 @@ public static boolean isHeaderAuthEnabled(final Properties props, * @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) { + public static Map buildTrustedAuthHeaders(final Properties props, final String configPrefix) { if (!isHeaderAuthEnabled(props, configPrefix)) { return Collections.emptyMap(); } - String propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX; - Map headers = new LinkedHashMap<>(); + String propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX; + Map headers = new LinkedHashMap<>(); for (String propertyName : sortedPropertyNames(props)) { if (!propertyName.startsWith(propertyPrefix)) { @@ -110,43 +101,31 @@ public static Map buildTrustedAuthHeaders(final Properties props } if (headers.isEmpty()) { - LOG.warn("Plugin header auth enabled for {} but no trusted headers " - + "could be resolved", configPrefix); + LOG.warn("Plugin header auth enabled for {} but no trusted headers could be resolved", configPrefix); } return Collections.unmodifiableMap(headers); } - private static void addConfiguredHeader(final Map headers, - final String configPrefix, final String headerName, - final String headerValue) { + 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); - return; - } - - if (headerValue.startsWith("spiffe://") - && !SpiffeIdUtil.isValidSpiffeId(headerValue)) { - LOG.warn("Resolved trusted header value for {} is not a " - + "well-formed SPIFFE ID", configPrefix); - return; + LOG.warn("Plugin header auth enabled for {} but trusted header {} has no resolvable value", configPrefix, headerName); + } else if (headerValue.startsWith("spiffe://") && !SpiffeIdUtil.isValidSpiffeId(headerValue)) { + LOG.warn("Resolved trusted header value for {} is not a well-formed SPIFFE ID", configPrefix); + } else { + headers.put(headerName, headerValue); } - - headers.put(headerName, headerValue); } private static String resolveConfiguredValue(final String valueSpec) { - String ret = null; + final String ret; if (StringUtils.isBlank(valueSpec)) { ret = null; } else if (valueSpec.startsWith(VALUE_PREFIX_FILE)) { - ret = SpiffeIdentityResolver.readFirstLine( - valueSpec.substring(VALUE_PREFIX_FILE.length())); + ret = SpiffeIdentityResolver.readFirstLine(valueSpec.substring(VALUE_PREFIX_FILE.length())); } else if (valueSpec.startsWith(VALUE_PREFIX_ENV)) { - ret = StringUtils.trimToNull( - System.getenv(valueSpec.substring(VALUE_PREFIX_ENV.length()))); + ret = StringUtils.trimToNull(System.getenv(valueSpec.substring(VALUE_PREFIX_ENV.length()))); } else { ret = StringUtils.trimToNull(valueSpec); } From 398af86ee5089662ad4b974182a08bed79b03502 Mon Sep 17 00:00:00 2001 From: ramk Date: Sun, 16 Aug 2026 23:43:43 +0530 Subject: [PATCH 14/16] RANGER-5723: Drop outbound SPIFFE validation and SpiffeIdentityResolver Address PR review: trusted outbound headers pass resolved values through without SPIFFE format checks; remove SpiffeIdentityResolver and obsolete tests for the old authn.spiffe.* resolution model. --- .../plugin/util/PluginHeaderAuthConfig.java | 48 +++++-- .../plugin/util/SpiffeIdentityResolver.java | 117 ------------------ .../util/PluginHeaderAuthConfigTest.java | 20 --- 3 files changed, 41 insertions(+), 144 deletions(-) delete mode 100644 common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdentityResolver.java 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 index 8b1546d0d7..4a87e3faef 100644 --- 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 @@ -23,6 +23,11 @@ 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.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -107,14 +112,16 @@ public static Map buildTrustedAuthHeaders(final Properties props return Collections.unmodifiableMap(headers); } - private static void addConfiguredHeader(final Map headers, final String configPrefix, final String headerName, final String headerValue) { + 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 if (headerValue.startsWith("spiffe://") && !SpiffeIdUtil.isValidSpiffeId(headerValue)) { - LOG.warn("Resolved trusted header value for {} is not a well-formed SPIFFE ID", configPrefix); - } else { - headers.put(headerName, headerValue); + LOG.warn("Plugin header auth enabled for {} but trusted header {} " + + "has no resolvable value", configPrefix, headerName); + return; } + + headers.put(headerName, headerValue); } private static String resolveConfiguredValue(final String valueSpec) { @@ -123,7 +130,7 @@ private static String resolveConfiguredValue(final String valueSpec) { if (StringUtils.isBlank(valueSpec)) { ret = null; } else if (valueSpec.startsWith(VALUE_PREFIX_FILE)) { - ret = SpiffeIdentityResolver.readFirstLine(valueSpec.substring(VALUE_PREFIX_FILE.length())); + 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 { @@ -133,6 +140,33 @@ private static String resolveConfiguredValue(final String 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; + } + private static List sortedPropertyNames(final Properties props) { List names = new ArrayList<>(); 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 deleted file mode 100644 index 0cbc342d32..0000000000 --- a/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdentityResolver.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 { - 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 static final Logger LOG = - LoggerFactory.getLogger(SpiffeIdentityResolver.class); - - 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; - } - - 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 index c8bfd139ba..98286c39a1 100644 --- 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 @@ -68,17 +68,6 @@ public void buildTrustedAuthHeadersEmptyWhenDisabled() { assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(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")); @@ -100,13 +89,4 @@ public void buildTrustedAuthHeadersEmptyWhenSpiffeIdUnresolved() { assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); } - - @Test - public void buildTrustedAuthHeadersEmptyWhenSpiffeIdMalformed() { - Properties props = new Properties(); - props.setProperty("ranger.ozone.authn.header.enabled", "true"); - props.setProperty("ranger.ozone.authn.header.X-Spiffe-Id", "spiffe://not-valid"); - - assertTrue(PluginHeaderAuthConfig.buildTrustedAuthHeaders(props, "ranger.ozone").isEmpty()); - } } From 52e4d3881aff6492ee4b191a5ef44e18e01c8977 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 16 Aug 2026 12:17:22 -0700 Subject: [PATCH 15/16] Update PluginHeaderAuthConfig.java --- .../plugin/util/PluginHeaderAuthConfig.java | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) 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 index 4a87e3faef..efd47e18ba 100644 --- 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 @@ -82,46 +82,46 @@ public static boolean isHeaderAuthEnabled(final Properties props, final String c * @return immutable header map; empty when auth is disabled or misconfigured */ public static Map buildTrustedAuthHeaders(final Properties props, final String configPrefix) { - if (!isHeaderAuthEnabled(props, configPrefix)) { - return Collections.emptyMap(); - } - - String propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX; - Map headers = new LinkedHashMap<>(); - - for (String propertyName : sortedPropertyNames(props)) { - if (!propertyName.startsWith(propertyPrefix)) { - continue; + final Map ret; + + if (isHeaderAuthEnabled(props, configPrefix)) { + String propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX; + Map headers = new LinkedHashMap<>(); + + for (String propertyName : sortedPropertyNames(props)) { + 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); } - - String headerName = propertyName.substring(propertyPrefix.length()); - - if (StringUtils.isBlank(headerName) || "enabled".equals(headerName)) { - continue; + + if (headers.isEmpty()) { + LOG.warn("Plugin header auth enabled for {} but no trusted headers could be resolved", configPrefix); } - - 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 Collections.unmodifiableMap(headers); + return ret; } - private static void addConfiguredHeader(final Map headers, - final String configPrefix, final String headerName, - final String headerValue) { + 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); - return; + LOG.warn("Plugin header auth enabled for {} but trusted header {} has no resolvable value", configPrefix, headerName); + } else { + headers.put(headerName, headerValue); } - - headers.put(headerName, headerValue); } private static String resolveConfiguredValue(final String valueSpec) { @@ -159,8 +159,7 @@ private static String readFirstNonBlankLine(final String filePath) { } } } catch (IOException ex) { - LOG.debug("Unable to read trusted header value from file {}", - filePath, ex); + LOG.debug("Unable to read trusted header value from file {}", filePath, ex); } } From 95e659e8141a085e66008d580717b56bc735d01c Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 16 Aug 2026 12:25:46 -0700 Subject: [PATCH 16/16] Update PluginHeaderAuthConfig.java --- .../ranger/plugin/util/PluginHeaderAuthConfig.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 index efd47e18ba..a3cf194359 100644 --- 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 @@ -87,27 +87,27 @@ public static Map buildTrustedAuthHeaders(final Properties props if (isHeaderAuthEnabled(props, configPrefix)) { String propertyPrefix = configPrefix + "." + PROP_HEADER_PREFIX; Map headers = new LinkedHashMap<>(); - + for (String propertyName : sortedPropertyNames(props)) { 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();