This is an automated email from the ASF dual-hosted git repository.
kumaab pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new 22fbc813d RANGER-5700: Support SPIFFE-based authn via HTTP headers
(#1096)
22fbc813d is described below
commit 22fbc813d576b2d386f7baaf51a1895c96b1bece
Author: Abhishek Kumar <[email protected]>
AuthorDate: Tue Jul 21 20:00:17 2026 -0700
RANGER-5700: Support SPIFFE-based authn via HTTP headers (#1096)
---
.../apache/ranger/plugin/util/SpiffeIdUtil.java | 130 ++++++++++++++
.../ranger/plugin/util/SpiffeIdUtilTest.java | 157 +++++++++++++++++
.../ranger-docker/scripts/pdp/ranger-pdp-site.xml | 6 +
pdp/conf.dist/README-k8s.md | 1 +
pdp/conf.dist/ranger-pdp-site.xml | 7 +
.../org/apache/ranger/pdp/RangerPdpServer.java | 1 +
.../apache/ranger/pdp/config/RangerPdpConfig.java | 4 +
.../ranger/pdp/config/RangerPdpConstants.java | 1 +
.../pdp/security/HttpHeaderAuthNHandler.java | 34 +++-
.../ranger/pdp/security/RangerPdpAuthNFilter.java | 3 +-
pdp/src/main/resources/ranger-pdp-default.xml | 7 +
.../pdp/security/HttpHeaderAuthNHandlerTest.java | 121 +++++++++++++-
.../web/filter/RangerHeaderPreAuthFilter.java | 44 ++++-
.../main/resources/conf.dist/ranger-admin-site.xml | 5 +
.../web/filter/TestRangerHeaderPreAuthFilter.java | 186 ++++++++++++++++++++-
.../TestRangerSecurityContextFormationFilter.java | 6 +-
16 files changed, 692 insertions(+), 21 deletions(-)
diff --git
a/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java
b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java
new file mode 100644
index 000000000..5c303364b
--- /dev/null
+++ b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java
@@ -0,0 +1,130 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Helper for validating a SPIFFE ID and extracting the service-account
identity from it.
+ *
+ * <p>A SPIFFE ID is expected in the exact form {@code
spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>};
+ * for service-to-service authentication the receiving server trusts the
workload
+ * identity carried in a header (e.g. {@code x-service-spiffe-id}) and uses
+ * the trailing service-account segment as the authenticated principal.
+ */
+public final class SpiffeIdUtil {
+ private static final String HEADER_NAMES_SEP = ",";
+
+ /**
+ * Matches {@code
spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>} using the character
+ * sets from the SPIFFE-ID specification:
+ * <ul>
+ * <li>trust-domain: lowercase letters, digits, {@code .}, {@code -},
{@code _} (the spec requires
+ * the trust domain to be lowercase);
+ * <li>namespace and service-account: ASCII letters (any case), digits,
{@code .}, {@code -}, {@code _}
+ * (SPIFFE path segments are case-sensitive).
+ * </ul>
+ * All segments must be non-empty. This is an allow-list of safe
characters, so whitespace, control
+ * characters and {@code /} are excluded, keeping the extracted principal
well-formed. Per the spec,
+ * the namespace and service-account path segments must not be the
relative modifiers {@code .} or
+ * {@code ..} (enforced by the leading negative look-aheads). The
service-account is exposed via the
+ * named group {@code sa}.
+ */
+ private static final Pattern SPIFFE_ID_PATTERN = Pattern.compile(
+
"^spiffe://[a-z0-9._-]+/ns/(?!\\.{1,2}/)[A-Za-z0-9._-]+/sa/(?<sa>(?!\\.{1,2}$)[A-Za-z0-9._-]+)$");
+
+ private SpiffeIdUtil() {
+ // to block instantiation
+ }
+
+ /**
+ * Parses a configured, comma-separated list of SPIFFE header names into
an ordered list of
+ * trimmed, non-empty names. Supports a single header name as a list of
length one.
+ *
+ * @param configValue the raw config value (e.g. {@code X-Spiffe-Id,
X-Workload-Id})
+ * @return an ordered, immutable list of header names; empty when the
input is blank
+ */
+ public static List<String> parseHeaderNames(String configValue) {
+ if (StringUtils.isBlank(configValue)) {
+ return Collections.emptyList();
+ }
+
+ List<String> ret = new ArrayList<>();
+
+ for (String name : configValue.split(HEADER_NAMES_SEP)) {
+ String trimmed = StringUtils.trimToNull(name);
+
+ if (trimmed != null) {
+ ret.add(trimmed);
+ }
+ }
+
+ return Collections.unmodifiableList(ret);
+ }
+
+ /**
+ * Validates that the given value is a well-formed SPIFFE ID in the exact
form
+ * {@code spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>}
with non-empty segments.
+ *
+ * @param spiffeId the raw SPIFFE ID value from the request header
+ * @return {@code true} if the value matches the expected SPIFFE ID
format; {@code false} otherwise
+ */
+ public static boolean isValidSpiffeId(String spiffeId) {
+ return matchSpiffeId(spiffeId) != null;
+ }
+
+ /**
+ * Extracts the service-account name from a SPIFFE ID such as
+ * {@code spiffe://my-cluster/ns/service-namespace/sa/service-sa}. The
value must match the exact
+ * SPIFFE ID format (see {@link #isValidSpiffeId(String)}); otherwise
{@code null} is returned.
+ *
+ * @param spiffeId the raw SPIFFE ID value from the request header
+ * @return the service-account segment (e.g. {@code service-sa}), or
+ * {@code null} if the input is blank or not a well-formed SPIFFE
ID
+ */
+ public static String extractServiceAccount(String spiffeId) {
+ Matcher matcher = matchSpiffeId(spiffeId);
+
+ return matcher != null ? matcher.group("sa") : null;
+ }
+
+ /**
+ * Matches the given value against the expected SPIFFE ID format,
computing the {@link Matcher} once.
+ *
+ * @param spiffeId the raw SPIFFE ID value from the request header
+ * @return a matched {@link Matcher} (with groups available) if the value
is a well-formed SPIFFE ID;
+ * {@code null} if the value is blank or does not match
+ */
+ private static Matcher matchSpiffeId(String spiffeId) {
+ if (StringUtils.isBlank(spiffeId)) {
+ return null;
+ }
+
+ Matcher matcher = SPIFFE_ID_PATTERN.matcher(spiffeId.trim());
+
+ return matcher.matches() ? matcher : null;
+ }
+}
diff --git
a/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
b/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
new file mode 100644
index 000000000..c9e76eb56
--- /dev/null
+++
b/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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 java.util.List;
+
+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;
+
+class SpiffeIdUtilTest {
+ private static final String VALID =
"spiffe://my-cluster/ns/service-namespace/sa/service-sa";
+
+ @Test
+ void isValidSpiffeId_acceptsExactFormat() {
+ assertTrue(SpiffeIdUtil.isValidSpiffeId(VALID));
+ assertTrue(SpiffeIdUtil.isValidSpiffeId(" " + VALID + " ")); //
trimmed
+ }
+
+ @Test
+ void isValidSpiffeId_rejectsMalformed() {
+ assertFalse(SpiffeIdUtil.isValidSpiffeId(null));
+ assertFalse(SpiffeIdUtil.isValidSpiffeId(""));
+ assertFalse(SpiffeIdUtil.isValidSpiffeId(" "));
+ assertFalse(SpiffeIdUtil.isValidSpiffeId("not-a-spiffe-id"));
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("https://my-cluster/ns/n/sa/s")); //
wrong scheme
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/sa/service-sa"));
// missing /ns/
+ assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/n"));
// missing /sa/
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns//sa/s")); //
empty namespace
+ assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe:///ns/n/sa/s"));
// empty trust domain
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/n/sa/")); //
empty service account
+ assertFalse(SpiffeIdUtil.isValidSpiffeId(VALID + "/"));
// trailing slash
+ assertFalse(SpiffeIdUtil.isValidSpiffeId(VALID + "/extra"));
// extra segment
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/sa/s/ns/n")); //
wrong order
+ }
+
+ @Test
+ void isValidSpiffeId_rejectsUnsafeOrIllegalCharacters() {
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/service
sa")); // embedded space
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/service\tsa"));
// embedded tab
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/service\nsa"));
// embedded newline
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/svc@admin"));
// illegal char '@'
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://My-Cluster/ns/prod/sa/service-sa"));
// trust domain must be lowercase per spec
+
+
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/service
sa"));
+
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/service\nsa"));
+ }
+
+ @Test
+ void isValidSpiffeId_rejectsRelativePathModifierSegments() {
+ // Per SPIFFE spec 2.2, path segments must not be the relative
modifiers '.' or '..'.
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/./sa/service-sa"));
// namespace '.'
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/../sa/service-sa"));
// namespace '..'
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/."));
// service-account '.'
+
assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/.."));
// service-account '..'
+
+
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/."));
+
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/../sa/service-sa"));
+
+ // Dots inside a segment (not a bare '.'/'..') remain valid.
+
assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/svc.v1"));
+ assertEquals("svc.v1",
SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/..prod/sa/svc.v1"));
+ }
+
+ @Test
+ void isValidSpiffeId_acceptsSpecAllowedCharacters() {
+ // SPIFFE path segments are case-sensitive and allow letters (any
case), digits, '.', '-', '_'.
+
assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster.example.com/ns/service-namespace/sa/service-sa"));
+ assertEquals("Service-SA",
SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/Prod/sa/Service-SA"));
// mixed case
+ assertEquals("svc_01",
SpiffeIdUtil.extractServiceAccount("spiffe://cluster1/ns/ns_2/sa/svc_01"));
// underscore
+ assertEquals("svc-01",
SpiffeIdUtil.extractServiceAccount("spiffe://cluster1/ns/ns-2/sa/svc-01"));
// hyphen/digits
+ }
+
+ @Test
+ void extractServiceAccount_returnsSaOnlyForValidId() {
+ assertEquals("service-sa", SpiffeIdUtil.extractServiceAccount(VALID));
+ assertEquals("service-sa", SpiffeIdUtil.extractServiceAccount(" " +
VALID + " "));
+ }
+
+ @Test
+ void extractServiceAccount_returnsNullForMalformedId() {
+ assertNull(SpiffeIdUtil.extractServiceAccount(null));
+ assertNull(SpiffeIdUtil.extractServiceAccount(""));
+ assertNull(SpiffeIdUtil.extractServiceAccount("not-a-spiffe-id"));
+
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/sa/service-sa"));
+
assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/n/sa/"));
+ assertNull(SpiffeIdUtil.extractServiceAccount(VALID + "/extra"));
+ }
+
+ @Test
+ void extractServiceAccount_handlesRealisticProductionIds() {
+ // Kubernetes trust domain with a DNS-style cluster name and typical
namespace/service-account names.
+ assertEquals("nginx-ingress",
+
SpiffeIdUtil.extractServiceAccount("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress"));
+
+ // Cloud/mesh style trust domain (e.g. the header value from the PR
description).
+ assertEquals("service-sa",
+
SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/service-namespace/sa/service-sa"));
+
+ // Istio-style default service account in an application namespace.
+ assertEquals("default",
+
SpiffeIdUtil.extractServiceAccount("spiffe://cluster.local/ns/payments/sa/default"));
+
+ // Namespace and service-account with digits and hyphens, as commonly
generated by platforms.
+ assertEquals("spark-driver-01",
+
SpiffeIdUtil.extractServiceAccount("spiffe://data-plane.internal/ns/team-analytics-42/sa/spark-driver-01"));
+
+
assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress"));
+
assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://cluster.local/ns/payments/sa/default"));
+ }
+
+ @Test
+ void rejectsSpecValidButNonConformingSpiffeId() {
+ // These are well-formed SPIFFE IDs per the SPIFFE spec (valid scheme
+ trust domain + path),
+ // but they do not follow the expected
spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>
+ // layout, so Ranger must reject them.
+ String workloadPath = "spiffe://example.org/workload/frontend"; //
arbitrary path, no /ns//sa/
+ String nsOnly = "spiffe://example.org/ns/prod"; // has
/ns/ but no /sa/ segment
+
+ assertFalse(SpiffeIdUtil.isValidSpiffeId(workloadPath));
+ assertFalse(SpiffeIdUtil.isValidSpiffeId(nsOnly));
+ assertNull(SpiffeIdUtil.extractServiceAccount(workloadPath));
+ assertNull(SpiffeIdUtil.extractServiceAccount(nsOnly));
+ }
+
+ @Test
+ void parseHeaderNames_handlesSingleAndMultiple() {
+ assertTrue(SpiffeIdUtil.parseHeaderNames(null).isEmpty());
+ assertTrue(SpiffeIdUtil.parseHeaderNames(" ").isEmpty());
+
+ assertEquals(List.of("x-spiffe"),
SpiffeIdUtil.parseHeaderNames("x-spiffe"));
+
+ List<String> names = SpiffeIdUtil.parseHeaderNames(" x-source ,
x-upstream ,");
+
+ assertEquals(List.of("x-source", "x-upstream"), names);
+ }
+}
diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
index 47fa64d13..2d097db17 100644
--- a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
+++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
@@ -115,6 +115,12 @@
<value>X-Forwarded-User</value>
</property>
+ <!-- service-to-service authentication via SPIFFE ID (comma-separated header
names) -->
+ <property>
+ <name>ranger.pdp.authn.header.spiffe</name>
+ <value>X-Spiffe-Id</value>
+ </property>
+
<!-- JWT bearer token authentication for incoming REST requests -->
<property>
<name>ranger.pdp.authn.jwt.enabled</name>
diff --git a/pdp/conf.dist/README-k8s.md b/pdp/conf.dist/README-k8s.md
index b630bec7d..95d7727ec 100644
--- a/pdp/conf.dist/README-k8s.md
+++ b/pdp/conf.dist/README-k8s.md
@@ -82,6 +82,7 @@ When configuring inbound PDP authentication in
`ranger-pdp-site.xml`, use the
- `ranger.pdp.authn.types`
- `ranger.pdp.authn.header.enabled`
- `ranger.pdp.authn.header.username`
+- `ranger.pdp.authn.header.spiffe`
- `ranger.pdp.authn.jwt.enabled`
- `ranger.pdp.authn.jwt.provider.url`
- `ranger.pdp.authn.jwt.public.key`
diff --git a/pdp/conf.dist/ranger-pdp-site.xml
b/pdp/conf.dist/ranger-pdp-site.xml
index 953584692..9d67b8eab 100644
--- a/pdp/conf.dist/ranger-pdp-site.xml
+++ b/pdp/conf.dist/ranger-pdp-site.xml
@@ -145,6 +145,13 @@
<description>HTTP header name from which the authenticated username is
read.</description>
</property>
+ <!-- optional; enables service-to-service authentication via SPIFFE ID -->
+ <property>
+ <name>ranger.pdp.authn.header.spiffe</name>
+ <value></value>
+ <description>Comma-separated list of HTTP header name(s) carrying a SPIFFE
ID (e.g. spiffe://cluster/ns/<ns>/sa/<service-account>). A single
header name is also valid. The trailing service-account segment is used as the
authenticated principal. When multiple headers are configured, the first one
carrying a well-formed SPIFFE ID is used. Consulted only when the username
header is absent.</description>
+ </property>
+
<!-- JWT bearer token authentication for incoming REST requests -->
<property>
<name>ranger.pdp.authn.jwt.enabled</name>
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
index dc74e0716..2f7ba67b1 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
@@ -225,6 +225,7 @@ private void addAuthFilter(Context ctx) {
// HTTP Header auth
authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED,
Boolean.toString(config.isHeaderAuthnEnabled()));
authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME,
config.getHeaderAuthnUsername());
+
authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE,
config.getHeaderAuthnSpiffe());
// JWT bearer token auth
authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED,
Boolean.toString(config.isJwtAuthnEnabled()));
diff --git
a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
index 25faa5ef1..d541d4268 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
@@ -143,6 +143,10 @@ public String getHeaderAuthnUsername() {
return get(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "");
}
+ public String getHeaderAuthnSpiffe() {
+ return get(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE, "");
+ }
+
// --- JWT bearer token auth ---
public boolean isJwtAuthnEnabled() {
return getBoolean(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, false);
diff --git
a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
index c19518540..a0c40f290 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
@@ -65,6 +65,7 @@ private RangerPdpConstants() {
public static final String PROP_AUTHN_HEADER_PREFIX = PROP_AUTHN_PREFIX
+ "header.";
public static final String PROP_AUTHN_HEADER_ENABLED =
PROP_AUTHN_HEADER_PREFIX + "enabled";
public static final String PROP_AUTHN_HEADER_USERNAME =
PROP_AUTHN_HEADER_PREFIX + "username";
+ public static final String PROP_AUTHN_HEADER_SPIFFE =
PROP_AUTHN_HEADER_PREFIX + "spiffe";
// JWT auth
public static final String PROP_AUTHN_JWT_PREFIX = PROP_AUTHN_PREFIX
+ "jwt.";
diff --git
a/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
index 0d8f8aacd..1761a0938 100644
---
a/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
+++
b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
@@ -21,12 +21,14 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.apache.ranger.plugin.util.SpiffeIdUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import java.util.List;
import java.util.Properties;
/**
@@ -44,24 +46,44 @@
public class HttpHeaderAuthNHandler implements PdpAuthNHandler {
private static final Logger LOG =
LoggerFactory.getLogger(HttpHeaderAuthNHandler.class);
- public static final String AUTH_TYPE = "HEADER";
+ public static final String AUTH_TYPE = "HEADER";
+ public static final String AUTH_TYPE_SPIFFE = "SPIFFE";
- private String usernameHeader;
+ private String usernameHeader;
+ private List<String> spiffeHeaders;
@Override
public void init(Properties config) {
usernameHeader =
config.getProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME);
+ spiffeHeaders =
SpiffeIdUtil.parseHeaderNames(config.getProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE));
- LOG.info("HttpHeaderAuthHandler initialized; username header={}",
usernameHeader);
+ LOG.info("HttpHeaderAuthHandler initialized; username header={},
spiffe headers={}", usernameHeader, spiffeHeaders);
}
@Override
public Result authenticate(HttpServletRequest request, HttpServletResponse
response) {
- String userName = request.getHeader(usernameHeader);
+ String userName = StringUtils.isNotBlank(usernameHeader) ?
request.getHeader(usernameHeader) : null;
- LOG.debug("authenticate(): user={} (from header {})", userName,
usernameHeader);
+ if (StringUtils.isNotBlank(userName)) {
+ LOG.debug("authenticate(): user={} (from header {})", userName,
usernameHeader);
- return StringUtils.isBlank(userName) ? Result.skip() :
Result.authenticated(userName.trim(), AUTH_TYPE);
+ return Result.authenticated(userName.trim(), AUTH_TYPE);
+ }
+
+ for (String spiffeHeader : spiffeHeaders) {
+ String spiffeId = request.getHeader(spiffeHeader);
+ String serviceAccount =
SpiffeIdUtil.extractServiceAccount(spiffeId);
+
+ if (StringUtils.isNotBlank(serviceAccount)) {
+ LOG.debug("authenticate(): service-account={} (from SPIFFE
header {})", serviceAccount, spiffeHeader);
+
+ return Result.authenticated(serviceAccount, AUTH_TYPE_SPIFFE);
+ } else if (StringUtils.isNotBlank(spiffeId)) {
+ LOG.warn("SPIFFE header '{}' value is not a well-formed SPIFFE
ID", spiffeHeader);
+ }
+ }
+
+ return Result.skip();
}
@Override
diff --git
a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
index e425464a7..00b8f4c70 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
@@ -151,7 +151,8 @@ private PdpAuthNHandler createHandler(String type,
FilterConfig filterConfig) {
switch (type) {
case "header":
ret = getBoolean(filterConfig,
RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED) &&
-
StringUtils.isNotBlank(filterConfig.getInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME))
+
(StringUtils.isNotBlank(filterConfig.getInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME))
||
+
StringUtils.isNotBlank(filterConfig.getInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE)))
? new HttpHeaderAuthNHandler() : null;
break;
case "jwt":
diff --git a/pdp/src/main/resources/ranger-pdp-default.xml
b/pdp/src/main/resources/ranger-pdp-default.xml
index 953584692..9d67b8eab 100644
--- a/pdp/src/main/resources/ranger-pdp-default.xml
+++ b/pdp/src/main/resources/ranger-pdp-default.xml
@@ -145,6 +145,13 @@
<description>HTTP header name from which the authenticated username is
read.</description>
</property>
+ <!-- optional; enables service-to-service authentication via SPIFFE ID -->
+ <property>
+ <name>ranger.pdp.authn.header.spiffe</name>
+ <value></value>
+ <description>Comma-separated list of HTTP header name(s) carrying a SPIFFE
ID (e.g. spiffe://cluster/ns/<ns>/sa/<service-account>). A single
header name is also valid. The trailing service-account segment is used as the
authenticated principal. When multiple headers are configured, the first one
carrying a well-formed SPIFFE ID is used. Consulted only when the username
header is absent.</description>
+ </property>
+
<!-- JWT bearer token authentication for incoming REST requests -->
<property>
<name>ranger.pdp.authn.jwt.enabled</name>
diff --git
a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
index 1bd2a5746..e122516bd 100644
---
a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
+++
b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
@@ -26,6 +26,9 @@
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -63,12 +66,128 @@ public void testAuthenticate_usesConfiguredHeaderName() {
assertEquals("bob", result.getUserName());
}
+ @Test
+ void testAuthenticate_usesSpiffeHeaderWhenUsernameAbsent() {
+ HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+ Properties config = new Properties();
+
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME,
"X-Authenticated-User");
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE,
"X-Spiffe-Id");
+
+ handler.init(config);
+
+ // Realistic production SPIFFE ID: DNS-style Kubernetes cluster trust
domain + namespace/service-account.
+ HttpServletRequest request = requestWithHeader("X-Spiffe-Id",
"spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress");
+ PdpAuthNHandler.Result result = handler.authenticate(request, null);
+
+ assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED,
result.getStatus());
+ assertEquals("nginx-ingress", result.getUserName());
+ assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE_SPIFFE,
result.getAuthType());
+ }
+
+ @Test
+ void testAuthenticate_usernameTakesPrecedenceOverSpiffe() {
+ HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+ Properties config = new Properties();
+
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME,
"X-Authenticated-User");
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE,
"X-Spiffe-Id");
+
+ handler.init(config);
+
+ Map<String, String> headers = new HashMap<>();
+
+ headers.put("X-Authenticated-User", "bob");
+ headers.put("X-Spiffe-Id",
"spiffe://my-cluster/ns/service-namespace/sa/service-sa");
+
+ PdpAuthNHandler.Result result =
handler.authenticate(requestWithHeaders(headers), null);
+
+ assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED,
result.getStatus());
+ assertEquals("bob", result.getUserName());
+ assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE, result.getAuthType());
+ }
+
+ @Test
+ void testAuthenticate_multipleSpiffeHeadersUsesFirstValid() {
+ HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+ Properties config = new Properties();
+
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE,
"X-Spiffe-Id, X-Workload-Id");
+
+ handler.init(config);
+
+ Map<String, String> headers = new HashMap<>();
+
+ headers.put("X-Spiffe-Id", "not-a-spiffe-id");
+ headers.put("X-Workload-Id",
"spiffe://my-cluster/ns/service-namespace/sa/service-sa");
+
+ PdpAuthNHandler.Result result =
handler.authenticate(requestWithHeaders(headers), null);
+
+ assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED,
result.getStatus());
+ assertEquals("service-sa", result.getUserName());
+ assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE_SPIFFE,
result.getAuthType());
+ }
+
+ @Test
+ void testAuthenticate_malformedSpiffeHeaderSkips() {
+ HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+ Properties config = new Properties();
+
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE,
"X-Spiffe-Id");
+
+ handler.init(config);
+
+ HttpServletRequest request = requestWithHeader("X-Spiffe-Id",
"not-a-spiffe-id");
+ PdpAuthNHandler.Result result = handler.authenticate(request, null);
+
+ assertEquals(PdpAuthNHandler.Result.Status.SKIP, result.getStatus());
+ assertNull(result.getUserName());
+ }
+
+ @Test
+ void testAuthenticate_specValidButNonConformingSpiffeHeaderSkips() {
+ HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+ Properties config = new Properties();
+
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE,
"X-Spiffe-Id");
+
+ handler.init(config);
+
+ // Valid SPIFFE ID per the SPIFFE spec, but not in the expected
/ns/<ns>/sa/<sa> layout.
+ HttpServletRequest request = requestWithHeader("X-Spiffe-Id",
"spiffe://example.org/workload/frontend");
+ PdpAuthNHandler.Result result = handler.authenticate(request, null);
+
+ assertEquals(PdpAuthNHandler.Result.Status.SKIP, result.getStatus());
+ assertNull(result.getUserName());
+ }
+
+ @Test
+ void testAuthenticate_spiffeHeaderWithIllegalCharsSkips() {
+ HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+ Properties config = new Properties();
+
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_SPIFFE,
"X-Spiffe-Id");
+
+ handler.init(config);
+
+ // Correct layout but the service-account contains whitespace, which
is not an allowed SPIFFE character.
+ HttpServletRequest request = requestWithHeader("X-Spiffe-Id",
"spiffe://my-cluster/ns/prod/sa/service sa");
+ PdpAuthNHandler.Result result = handler.authenticate(request, null);
+
+ assertEquals(PdpAuthNHandler.Result.Status.SKIP, result.getStatus());
+ assertNull(result.getUserName());
+ }
+
private static HttpServletRequest requestWithHeader(String expectedHeader,
String headerValue) {
+ return requestWithHeaders(Collections.singletonMap(expectedHeader,
headerValue));
+ }
+
+ private static HttpServletRequest requestWithHeaders(Map<String, String>
headers) {
InvocationHandler invocationHandler = (proxy, method, args) -> {
String methodName = method.getName();
if ("getHeader".equals(methodName)) {
- return expectedHeader.equals(args[0]) ? headerValue : null;
+ return headers.get(args[0]);
} else if ("getHeaders".equals(methodName) ||
"getHeaderNames".equals(methodName)) {
return java.util.Collections.emptyEnumeration();
} else if ("getMethod".equals(methodName)) {
diff --git
a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
index 920f2ad8a..3cf984f7d 100644
---
a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
+++
b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
@@ -22,6 +22,7 @@
import org.apache.ranger.biz.UserMgr;
import org.apache.ranger.common.PropertiesUtil;
import org.apache.ranger.entity.XXAuthSession;
+import org.apache.ranger.plugin.util.SpiffeIdUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -51,10 +52,12 @@ public class RangerHeaderPreAuthFilter extends
GenericFilterBean {
public static final String PROP_HEADER_AUTH_ENABLED =
"ranger.admin.authn.header.enabled";
public static final String PROP_USERNAME_HEADER_NAME =
"ranger.admin.authn.header.username";
+ public static final String PROP_SPIFFE_HEADER_NAME =
"ranger.admin.authn.header.spiffe";
public static final String PROP_REQUEST_ID_HEADER_NAME =
"ranger.admin.authn.header.requestid";
- private boolean headerAuthEnabled;
- private String userNameHeaderName;
+ private boolean headerAuthEnabled;
+ private String userNameHeaderName;
+ private List<String> spiffeHeaderNames;
@Autowired
UserMgr userMgr;
@@ -65,9 +68,10 @@ protected void initialize() {
if (headerAuthEnabled) {
userNameHeaderName =
PropertiesUtil.getProperty(PROP_USERNAME_HEADER_NAME);
+ spiffeHeaderNames =
SpiffeIdUtil.parseHeaderNames(PropertiesUtil.getProperty(PROP_SPIFFE_HEADER_NAME));
- if (StringUtils.isBlank(userNameHeaderName)) {
- LOG.warn("Disabling header-based authentication, as
configuration {} is not set", PROP_USERNAME_HEADER_NAME);
+ if (StringUtils.isBlank(userNameHeaderName) &&
spiffeHeaderNames.isEmpty()) {
+ LOG.warn("Disabling header-based authentication, as neither {}
nor {} is set", PROP_USERNAME_HEADER_NAME, PROP_SPIFFE_HEADER_NAME);
headerAuthEnabled = false;
}
@@ -81,7 +85,7 @@ public void doFilter(ServletRequest request, ServletResponse
response, FilterCha
if (existingAuthn == null || !existingAuthn.isAuthenticated()) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
- String username =
StringUtils.trimToNull(httpRequest.getHeader(userNameHeaderName));
+ String username =
resolvePrincipal(httpRequest);
if (StringUtils.isNotBlank(username)) {
List<GrantedAuthority> grantedAuthorities =
getAuthoritiesFromRanger(username);
@@ -94,7 +98,7 @@ public void doFilter(ServletRequest request, ServletResponse
response, FilterCha
LOG.debug("Authenticated request using trusted headers for
user={}", username);
} else {
- LOG.debug("Username header '{}' is missing or empty in the
request!", userNameHeaderName);
+ LOG.debug("No trusted identity header found in the
request!");
}
}
} else {
@@ -104,6 +108,34 @@ public void doFilter(ServletRequest request,
ServletResponse response, FilterCha
chain.doFilter(request, response);
}
+ /**
+ * Resolves the principal from trusted headers. The username header (user
identity) takes
+ * precedence; when it is absent, the SPIFFE header (service identity) is
used and
+ * the trailing service-account segment of the SPIFFE ID becomes the
principal.
+ */
+ private String resolvePrincipal(HttpServletRequest httpRequest) {
+ String username = StringUtils.isNotBlank(userNameHeaderName) ?
StringUtils.trimToNull(httpRequest.getHeader(userNameHeaderName)) : null;
+
+ if (StringUtils.isNotBlank(username)) {
+ return username;
+ }
+
+ for (String spiffeHeaderName : spiffeHeaderNames) {
+ String spiffeId =
StringUtils.trimToNull(httpRequest.getHeader(spiffeHeaderName));
+ String serviceAccount =
SpiffeIdUtil.extractServiceAccount(spiffeId);
+
+ if (StringUtils.isNotBlank(serviceAccount)) {
+ LOG.debug("Resolved service-account '{}' from SPIFFE header
'{}'", serviceAccount, spiffeHeaderName);
+
+ return serviceAccount;
+ } else if (StringUtils.isNotBlank(spiffeId)) {
+ LOG.warn("SPIFFE header '{}' value is not a well-formed SPIFFE
ID", spiffeHeaderName);
+ }
+ }
+
+ return null;
+ }
+
/**
* Loads authorities from Ranger DB
*/
diff --git a/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
b/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
index 388ce1674..460c567e8 100644
--- a/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
+++ b/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
@@ -323,6 +323,11 @@
<name>ranger.admin.authn.header.username</name>
<value></value>
</property>
+ <property>
+ <name>ranger.admin.authn.header.spiffe</name>
+ <value></value>
+ <description>Comma-separated list of HTTP header name(s)
carrying a SPIFFE ID (e.g.
spiffe://cluster/ns/<ns>/sa/<service-account>). A single header
name is also valid. The trailing service-account segment is used as the
authenticated principal. When multiple headers are configured, the first one
carrying a well-formed SPIFFE ID is used. Consulted only when the username
header is absent.</description>
+ </property>
<property>
<name>ranger.admin.authn.header.requestid</name>
<value></value>
diff --git
a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
index c721c30f0..675970dd2 100644
---
a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
+++
b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
@@ -66,6 +66,7 @@ public void tearDown() {
PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED);
PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME);
+
PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME);
PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME);
}
@@ -91,7 +92,7 @@ public void testDoFilter_disabled_passesThrough() throws
Exception {
@Test
public void testDoFilter_enabled_missingUsername_passesThrough() throws
Exception {
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
-
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
"x-awc-username");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
"X-Forwarded-User");
RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
UserMgr userMgr = mock(UserMgr.class);
@@ -115,7 +116,7 @@ public void
testDoFilter_enabled_missingUsername_passesThrough() throws Exceptio
@Test
public void
testDoFilter_enabled_withUsername_setsAuthenticationFromRangerDbRoles() throws
Exception {
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
-
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
"x-awc-username");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
"X-Forwarded-User");
RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
UserMgr userMgr = mock(UserMgr.class);
@@ -128,7 +129,7 @@ public void
testDoFilter_enabled_withUsername_setsAuthenticationFromRangerDbRole
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
- when(request.getHeader("x-awc-username")).thenReturn("joeuser");
+ when(request.getHeader("X-Forwarded-User")).thenReturn("joeuser");
FilterChain chain = new FilterChain() {
@Override
@@ -151,10 +152,187 @@ public void doFilter(ServletRequest req, ServletResponse
res) {
filter.doFilter(request, response, chain);
}
+ @Test
+ public void
testDoFilter_enabled_withSpiffeHeader_setsServiceAccountAuthentication() throws
Exception {
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
"X-Forwarded-User");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
"X-Spiffe-Id");
+
+ RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
+ UserMgr userMgr = mock(UserMgr.class);
+
+ filter.userMgr = userMgr;
+ filter.initialize();
+
+
when(userMgr.getRolesByLoginId("nginx-ingress")).thenReturn(Collections.singletonList("ROLE_USER"));
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ when(request.getHeader("X-Forwarded-User")).thenReturn(null);
+ // Realistic production SPIFFE ID: DNS-style Kubernetes cluster trust
domain + namespace/service-account.
+
when(request.getHeader("X-Spiffe-Id")).thenReturn("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress");
+
+ FilterChain chain = new FilterChain() {
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res) {
+ org.springframework.security.core.Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
+
+ assertNotNull(auth);
+ assertTrue(auth instanceof RangerAuthenticationToken);
+ RangerAuthenticationToken rangerAuth =
(RangerAuthenticationToken) auth;
+ assertEquals(XXAuthSession.AUTH_TYPE_TRUSTED_PROXY,
rangerAuth.getAuthType());
+ assertEquals("nginx-ingress", auth.getName());
+ }
+ };
+
+ filter.doFilter(request, response, chain);
+ }
+
+ @Test
+ public void testDoFilter_enabled_usernameHeaderTakesPrecedenceOverSpiffe()
throws Exception {
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
"X-Forwarded-User");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
"X-Spiffe-Id");
+
+ RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
+ UserMgr userMgr = mock(UserMgr.class);
+
+ filter.userMgr = userMgr;
+ filter.initialize();
+
+
when(userMgr.getRolesByLoginId("joeuser")).thenReturn(Collections.singletonList("ROLE_USER"));
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ when(request.getHeader("X-Forwarded-User")).thenReturn("joeuser");
+
+ FilterChain chain = new FilterChain() {
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res) {
+ org.springframework.security.core.Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
+
+ assertNotNull(auth);
+ assertEquals("joeuser", auth.getName());
+ }
+ };
+
+ filter.doFilter(request, response, chain);
+
+ verify(userMgr, never()).getRolesByLoginId("service-sa");
+ }
+
+ @Test
+ public void testDoFilter_enabled_multipleSpiffeHeaders_usesFirstValid()
throws Exception {
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
"X-Spiffe-Id, X-Workload-Id");
+
+ RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
+ UserMgr userMgr = mock(UserMgr.class);
+
+ filter.userMgr = userMgr;
+ filter.initialize();
+
+
when(userMgr.getRolesByLoginId("service-sa")).thenReturn(Collections.singletonList("ROLE_USER"));
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+
+ when(request.getHeader("X-Spiffe-Id")).thenReturn("not-a-spiffe-id");
+
when(request.getHeader("X-Workload-Id")).thenReturn("spiffe://my-cluster/ns/service-namespace/sa/service-sa");
+
+ FilterChain chain = new FilterChain() {
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res) {
+ org.springframework.security.core.Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
+
+ assertNotNull(auth);
+ assertEquals("service-sa", auth.getName());
+ }
+ };
+
+ filter.doFilter(request, response, chain);
+ }
+
+ @Test
+ public void testDoFilter_enabled_malformedSpiffeHeader_passesThrough()
throws Exception {
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
"X-Spiffe-Id");
+
+ RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
+ UserMgr userMgr = mock(UserMgr.class);
+
+ filter.userMgr = userMgr;
+ filter.initialize();
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+ when(request.getHeader("X-Spiffe-Id")).thenReturn("not-a-spiffe-id");
+
+ filter.doFilter(request, response, chain);
+
+ verify(chain).doFilter(request, response);
+ verify(userMgr, never()).getRolesByLoginId(anyString());
+ assertNull(SecurityContextHolder.getContext().getAuthentication());
+ }
+
+ @Test
+ public void
testDoFilter_enabled_specValidButNonConformingSpiffeHeader_passesThrough()
throws Exception {
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
"X-Spiffe-Id");
+
+ RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
+ UserMgr userMgr = mock(UserMgr.class);
+
+ filter.userMgr = userMgr;
+ filter.initialize();
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+ // Valid SPIFFE ID per the SPIFFE spec, but not in the expected
/ns/<ns>/sa/<sa> layout.
+
when(request.getHeader("X-Spiffe-Id")).thenReturn("spiffe://example.org/workload/frontend");
+
+ filter.doFilter(request, response, chain);
+
+ verify(chain).doFilter(request, response);
+ verify(userMgr, never()).getRolesByLoginId(anyString());
+ assertNull(SecurityContextHolder.getContext().getAuthentication());
+ }
+
+ @Test
+ public void
testDoFilter_enabled_spiffeHeaderWithIllegalChars_passesThrough() throws
Exception {
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME,
"X-Spiffe-Id");
+
+ RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
+ UserMgr userMgr = mock(UserMgr.class);
+
+ filter.userMgr = userMgr;
+ filter.initialize();
+
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ FilterChain chain = mock(FilterChain.class);
+
+ // Correct layout but the service-account contains whitespace, which
is not an allowed SPIFFE character.
+
when(request.getHeader("X-Spiffe-Id")).thenReturn("spiffe://my-cluster/ns/prod/sa/service
sa");
+
+ filter.doFilter(request, response, chain);
+
+ verify(chain).doFilter(request, response);
+ verify(userMgr, never()).getRolesByLoginId(anyString());
+ assertNull(SecurityContextHolder.getContext().getAuthentication());
+ }
+
@Test
public void
testDoFilter_enabled_existingAuthenticatedContext_doesNotOverrideAuthentication()
throws Exception {
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED,
"true");
-
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
"x-awc-username");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME,
"X-Forwarded-User");
RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter();
UserMgr userMgr = mock(UserMgr.class);
diff --git
a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java
b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java
index ff9f059bc..e468de58f 100644
---
a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java
+++
b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java
@@ -184,7 +184,7 @@ public void testGetAuthType_reflectionVariants() throws
Exception {
@Test
public void
testDoFilter_authenticated_createsSecurityContextAndUserSession() throws
Exception {
-
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME,
"x-awc-requestid");
+
PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME,
"X-Request-Id");
try {
RangerSecurityContextFormationFilter filter = new
RangerSecurityContextFormationFilter();
@@ -212,7 +212,7 @@ public void
testDoFilter_authenticated_createsSecurityContextAndUserSession() th
Mockito.when(req.getSession(false)).thenReturn(session);
Mockito.when(session.getAttribute(RangerSecurityContextFormationFilter.AKA_SC_SESSION_KEY)).thenReturn(null);
Mockito.when(req.getHeader(RangerSecurityContextFormationFilter.USER_AGENT)).thenReturn("Mozilla/5.0");
-
Mockito.when(req.getHeader("x-awc-requestid")).thenReturn("awc-request-1");
+
Mockito.when(req.getHeader("X-Request-Id")).thenReturn("request-1");
Mockito.when(req.getRequestURI()).thenReturn("/secure");
Mockito.when(httpUtil.getDeviceType(req)).thenReturn(RangerCommonEnums.DEVICE_BROWSER);
@@ -229,7 +229,7 @@ public void doFilter(ServletRequest servletRequest,
ServletResponse servletRespo
assertNotNull(ctx);
assertNotNull(ctx.getRequestContext());
- assertEquals("awc-request-1",
ctx.getRequestContext().getServerRequestId());
+ assertEquals("request-1",
ctx.getRequestContext().getServerRequestId());
assertSame(userSession, ctx.getUserSession());
}
};