Skip to content

Commit 3a75905

Browse files
authored
RANGER-5816: Add support for header based authentication in audit-ingestor service (#1279)
1 parent e705ae8 commit 3a75905

6 files changed

Lines changed: 432 additions & 1 deletion

File tree

‎audit-server/audit-ingestor/pom.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,12 @@
394394
<version>${junit.jupiter.version}</version>
395395
<scope>test</scope>
396396
</dependency>
397+
<dependency>
398+
<groupId>org.mockito</groupId>
399+
<artifactId>mockito-core</artifactId>
400+
<version>${mockito.version}</version>
401+
<scope>test</scope>
402+
</dependency>
397403
</dependencies>
398404

399405
<build>

‎audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/rest/AuditREST.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public Response logAccessAudit(@QueryParam("serviceName") String serviceName, @Q
188188
.entity(buildErrorResponse("Authentication required to send audit events"))
189189
.build();
190190
} else if (!isAllowedServiceUser(serviceName, authenticatedUser)) {
191-
LOG.error("Unauthorized user: user={} is authorized report audit logs for service={}. Rejecting audit request.", authenticatedUser, serviceName);
191+
LOG.error("Unauthorized user: user={} is not authorized to report audit logs for service={}. Rejecting audit request.", authenticatedUser, serviceName);
192192

193193
ret = Response.status(Response.Status.FORBIDDEN)
194194
.entity(buildErrorResponse("User is not authorized to send audit events"))
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.ranger.audit.security;
21+
22+
import org.apache.commons.lang3.StringUtils;
23+
import org.apache.ranger.audit.server.AuditServerConfig;
24+
import org.apache.ranger.audit.server.AuditServerConstants;
25+
import org.apache.ranger.plugin.util.SpiffeIdUtil;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
29+
import org.springframework.security.core.Authentication;
30+
import org.springframework.security.core.GrantedAuthority;
31+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
32+
import org.springframework.security.core.context.SecurityContextHolder;
33+
import org.springframework.security.core.userdetails.User;
34+
import org.springframework.security.core.userdetails.UserDetails;
35+
import org.springframework.security.web.authentication.WebAuthenticationDetails;
36+
import org.springframework.web.filter.GenericFilterBean;
37+
38+
import javax.annotation.PostConstruct;
39+
import javax.servlet.FilterChain;
40+
import javax.servlet.ServletException;
41+
import javax.servlet.ServletRequest;
42+
import javax.servlet.ServletResponse;
43+
import javax.servlet.http.HttpServletRequest;
44+
45+
import java.io.IOException;
46+
import java.util.Collections;
47+
import java.util.List;
48+
49+
/**
50+
* Authenticates audit REST requests using identity headers set by a trusted proxy. The resolved
51+
* principal is still subject to the per-service allowed users check in the audit REST API.
52+
*/
53+
public class AuditHeaderPreAuthFilter extends GenericFilterBean {
54+
private static final Logger LOG = LoggerFactory.getLogger(AuditHeaderPreAuthFilter.class);
55+
56+
public static final String PROP_HEADER_AUTH_ENABLED = AuditServerConstants.PROP_PREFIX_AUDIT_SERVER + "authn.header.enabled";
57+
public static final String PROP_USERNAME_HEADER_NAME = AuditServerConstants.PROP_PREFIX_AUDIT_SERVER + "authn.header.username";
58+
public static final String PROP_SPIFFE_HEADER_NAME = AuditServerConstants.PROP_PREFIX_AUDIT_SERVER + "authn.header.spiffe";
59+
60+
private static final String DEFAULT_AUDIT_ROLE = "ROLE_USER";
61+
62+
private boolean headerAuthEnabled;
63+
private String userNameHeaderName;
64+
private List<String> spiffeHeaderNames;
65+
66+
@PostConstruct
67+
protected void initialize() {
68+
AuditServerConfig auditConfig = AuditServerConfig.getInstance();
69+
70+
headerAuthEnabled = auditConfig.getBoolean(PROP_HEADER_AUTH_ENABLED, false);
71+
72+
if (headerAuthEnabled) {
73+
userNameHeaderName = StringUtils.trimToNull(auditConfig.get(PROP_USERNAME_HEADER_NAME));
74+
spiffeHeaderNames = SpiffeIdUtil.parseHeaderNames(auditConfig.get(PROP_SPIFFE_HEADER_NAME));
75+
76+
if (userNameHeaderName == null && spiffeHeaderNames.isEmpty()) {
77+
LOG.warn("Disabling header-based authentication, as neither {} nor {} is set", PROP_USERNAME_HEADER_NAME, PROP_SPIFFE_HEADER_NAME);
78+
79+
headerAuthEnabled = false;
80+
} else {
81+
LOG.info("Header-based authentication is enabled: usernameHeader={}, spiffeHeaders={}", userNameHeaderName, spiffeHeaderNames);
82+
}
83+
}
84+
}
85+
86+
@Override
87+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
88+
if (headerAuthEnabled) {
89+
Authentication existingAuthn = SecurityContextHolder.getContext().getAuthentication();
90+
91+
if (existingAuthn == null || !existingAuthn.isAuthenticated()) {
92+
HttpServletRequest httpRequest = (HttpServletRequest) request;
93+
String username = resolvePrincipal(httpRequest);
94+
95+
if (username != null) {
96+
List<GrantedAuthority> grantedAuths = Collections.singletonList(new SimpleGrantedAuthority(DEFAULT_AUDIT_ROLE));
97+
UserDetails principal = new User(username, "", grantedAuths);
98+
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(principal, "", grantedAuths);
99+
100+
authToken.setDetails(new WebAuthenticationDetails(httpRequest));
101+
102+
SecurityContextHolder.getContext().setAuthentication(authToken);
103+
104+
LOG.debug("Authenticated request using trusted headers for user={}", username);
105+
} else {
106+
LOG.debug("No trusted identity header found in the request!");
107+
}
108+
}
109+
} else {
110+
LOG.debug("Header-based authentication is disabled!");
111+
}
112+
113+
chain.doFilter(request, response);
114+
}
115+
116+
/**
117+
* Resolves the principal from trusted headers. The username header (user identity) takes
118+
* precedence; when it is absent, the SPIFFE header (service identity) is used and the
119+
* full SPIFFE ID becomes the principal (SPIFFE IDs are used as usernames in Ranger).
120+
*/
121+
private String resolvePrincipal(HttpServletRequest httpRequest) {
122+
String ret = userNameHeaderName != null ? StringUtils.trimToNull(httpRequest.getHeader(userNameHeaderName)) : null;
123+
124+
if (ret == null) {
125+
for (String spiffeHeaderName : spiffeHeaderNames) {
126+
String spiffeId = StringUtils.trimToNull(httpRequest.getHeader(spiffeHeaderName));
127+
128+
if (SpiffeIdUtil.isValidSpiffeId(spiffeId)) {
129+
LOG.debug("Resolved SPIFFE ID '{}' from header '{}'", spiffeId, spiffeHeaderName);
130+
131+
ret = spiffeId;
132+
133+
break;
134+
} else if (spiffeId != null) {
135+
LOG.warn("SPIFFE header '{}' value is not a well-formed SPIFFE ID", spiffeHeaderName);
136+
}
137+
}
138+
}
139+
140+
return ret;
141+
}
142+
}

‎audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,29 @@
182182
<description>Expected audiences for JWT validation (comma-separated)</description>
183183
</property>
184184

185+
<!-- Trusted Header Auth Configs -->
186+
<property>
187+
<name>ranger.audit.ingestor.authn.header.enabled</name>
188+
<value>false</value>
189+
<description>
190+
Enable authentication using identity headers set by a trusted proxy (e.g. service mesh or ingress gateway).
191+
Enable only when all requests reach the audit ingestor through that proxy, and the proxy strips these headers from client requests.
192+
The authenticated user must be listed in ranger.audit.ingestor.service.&lt;serviceName&gt;.allowed.users to post audits.
193+
</description>
194+
</property>
195+
196+
<property>
197+
<name>ranger.audit.ingestor.authn.header.username</name>
198+
<value></value>
199+
<description>Name of the HTTP header (e.g. X-Forwarded-User) carrying the authenticated username. Takes precedence over the SPIFFE header(s).</description>
200+
</property>
201+
202+
<property>
203+
<name>ranger.audit.ingestor.authn.header.spiffe</name>
204+
<value></value>
205+
<description>Comma-separated list of HTTP header name(s) carrying a SPIFFE ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single header name is also valid. The full SPIFFE ID 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>
206+
</property>
207+
185208
<property>
186209
<name>ranger.audit.ingestor.service.dev_hdfs.allowed.users</name>
187210
<value>hdfs</value>

‎audit-server/audit-ingestor/src/main/webapp/WEB-INF/security-applicationContext.xml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ http://www.springframework.org/schema/util/spring-util-4.3.xsd">
4646
<intercept-url pattern="/api/audit/status" access="permitAll()"/>
4747
<intercept-url pattern="/api/audit/**" access="isAuthenticated()"/>
4848
<security:access-denied-handler error-page="/index.jsp"/>
49+
<custom-filter ref="auditHeaderPreAuthFilter" position="PRE_AUTH_FILTER" />
4950
<custom-filter ref="auditJwtAuthFilter" after="SERVLET_API_SUPPORT_FILTER" />
5051
<custom-filter ref="auditDelegationTokenFilter" before="ANONYMOUS_FILTER" />
5152
</security:http>
@@ -54,6 +55,8 @@ http://www.springframework.org/schema/util/spring-util-4.3.xsd">
5455
<beans:constructor-arg value="/index.jsp"/>
5556
</beans:bean>
5657

58+
<beans:bean id="auditHeaderPreAuthFilter" class="org.apache.ranger.audit.security.AuditHeaderPreAuthFilter"/>
59+
5760
<beans:bean id="auditJwtAuthFilter" class="org.apache.ranger.audit.security.AuditJwtAuthFilter"/>
5861

5962
<beans:bean id="auditDelegationTokenFilter" class="org.apache.ranger.audit.security.AuditDelegationTokenFilter"/>

0 commit comments

Comments
 (0)