Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
import org.apache.commons.lang3.StringUtils;
import org.apache.ranger.authz.handler.RangerAuth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
Expand All @@ -35,6 +37,7 @@
* Default implementation of Ranger JWT authentication
*/
public class RangerDefaultJwtAuthHandler extends RangerJwtAuthHandler {
private static final Logger LOG = LoggerFactory.getLogger(RangerDefaultJwtAuthHandler.class);
protected static final String AUTHORIZATION_HEADER = "Authorization";
protected static final String DO_AS_PARAMETER = "doAs";

Expand Down Expand Up @@ -81,12 +84,42 @@ public RangerAuth authenticate(HttpServletRequest httpServletRequest) {
String jwtAuthHeaderStr = getJwtAuthHeader(httpServletRequest);
String jwtCookieStr = StringUtils.isBlank(jwtAuthHeaderStr) ? getJwtCookie(httpServletRequest) : null;
String doAsUser = httpServletRequest.getParameter(DO_AS_PARAMETER);
String username = authenticate(jwtAuthHeaderStr, jwtCookieStr, doAsUser);
// authenticate against the JWT first to get the real (token-verified) user
String realUser = authenticate(jwtAuthHeaderStr, jwtCookieStr);

if (username != null) {
rangerAuth = new RangerAuth(username, RangerAuth.AuthType.JWT_JWKS);
}
if (realUser != null) {
String effectiveUser = realUser;

if (StringUtils.isNotBlank(doAsUser)) {
LOG.debug("RangerDefaultJwtAuthHandler.authenticate(): doAs=[{}] requested. isProxyEnabled=[{}]", doAsUser, isProxyEnabled());

if (!isProxyEnabled()) {
LOG.warn("doAs [{}] requested but trusted proxy is not enabled. Ignoring doAs, proceeding with real user [{}].",
doAsUser, effectiveUser);
} else {
LOG.debug("RangerDefaultJwtAuthHandler.authenticate(): Calling authorizeProxyUser: realUser=[{}], doAs=[{}], remoteAddr=[{}]",
realUser, doAsUser, httpServletRequest.getRemoteAddr());
// Check: is realUser authorized to impersonate doAsUser
if (!authorizeProxyUser(realUser, doAsUser, httpServletRequest.getRemoteAddr())) {
LOG.warn("RangerDefaultJwtAuthHandler.authenticate(): doAs=[{}] not authorized for realUser=[{}]. Rejecting.", doAsUser, realUser);
return null;
}
//Checks passed → switch to doAs user
effectiveUser = doAsUser.trim();
LOG.info("JWT doAs authorized: effectiveUser=[{}], realUser=[{}]", effectiveUser, realUser);
Comment on lines +100 to +109
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doAsUser is trimmed only after the proxy authorization check, but the authorization call uses the untrimmed request parameter. This can cause legitimate impersonation to be rejected (or mismatched) when the parameter has leading/trailing whitespace. Trim (and ideally normalize) doAsUser once up-front and use the trimmed value consistently for authorizeProxyUser(...), logging, and effectiveUser.

Copilot uses AI. Check for mistakes.
}
}

rangerAuth = new RangerAuth(effectiveUser, RangerAuth.AuthType.JWT_JWKS);
}
return rangerAuth;
}

protected boolean isProxyEnabled() {
return false;
}

protected boolean authorizeProxyUser(String realUser, String doAsUser, String remoteAddr) {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public void initialize(final Properties config) throws Exception {

public abstract ConfigurableJWTProcessor<SecurityContext> getJwtProcessor(JWSKeySelector<SecurityContext> keySelector);

protected String authenticate(final String jwtAuthHeader, final String jwtCookie, final String doAsUser) {
protected String authenticate(final String jwtAuthHeader, final String jwtCookie) {
if (LOG.isDebugEnabled()) {
LOG.debug("===>>> RangerJwtAuthHandler.authenticate()");
}
Expand All @@ -119,17 +119,10 @@ protected String authenticate(final String jwtAuthHeader, final String jwtCookie
final SignedJWT jwtToken = SignedJWT.parse(serializedJWT);
boolean valid = validateToken(jwtToken);
if (valid) {
String userName;

if (StringUtils.isNotBlank(doAsUser)) {
userName = doAsUser.trim();
} else {
userName = jwtToken.getJWTClaimsSet().getSubject();
}
String userName = jwtToken.getJWTClaimsSet().getSubject();

if (LOG.isDebugEnabled()) {
LOG.debug("RangerJwtAuthHandler.authenticate(): Issuing AuthenticationToken for user: [{}]", userName);
LOG.debug("RangerJwtAuthHandler.authenticate(): Authentication successful for user [{}] and doAs user is [{}]", jwtToken.getJWTClaimsSet().getSubject(), doAsUser);
}
return userName;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
*/
package org.apache.ranger.security.web.filter;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authorize.AuthorizationException;
import org.apache.hadoop.security.authorize.ProxyUsers;
import org.apache.ranger.authz.handler.RangerAuth;
import org.apache.ranger.authz.handler.jwt.RangerDefaultJwtAuthHandler;
import org.apache.ranger.authz.handler.jwt.RangerJwtAuthHandler;
Expand Down Expand Up @@ -75,6 +79,9 @@ public void initialize() {
config.setProperty(RangerJwtAuthHandler.KEY_JWT_AUDIENCES, PropertiesUtil.getProperty(RangerSSOAuthenticationFilter.JWT_AUDIENCES, ""));

super.initialize(config);

Configuration conf = getProxyuserConfiguration();
ProxyUsers.refreshSuperUserGroupsConfiguration(conf, "ranger.proxyuser.");
} catch (Exception e) {
LOG.error("Failed to initialize Ranger Admin JWT Auth Filter.", e);
}
Expand Down Expand Up @@ -117,6 +124,36 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
}
}

@Override
protected boolean isProxyEnabled() {
return PropertiesUtil.getBooleanProperty("ranger.authentication.allow.trustedproxy", false);
}

@Override
protected boolean authorizeProxyUser(String realUser, String doAsUser, String remoteAddr) {
try {
UserGroupInformation ugi = UserGroupInformation.createRemoteUser(realUser);
ugi = UserGroupInformation.createProxyUser(doAsUser, ugi);
ProxyUsers.authorize(ugi, remoteAddr);
LOG.debug("RangerJwtAuthFilter.authorizeProxyUser(): ProxyUsers.authorize SUCCEEDED for realUser=[{}], doAs=[{}]",
realUser, doAsUser);
return true;
} catch (AuthorizationException ex) {
LOG.warn("JWT ProxyUsers.authorize failed for doAs=[{}], realUser=[{}]: {}", doAsUser, realUser, ex.getMessage());
Comment on lines +134 to +142
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authorizeProxyUser() passes doAsUser directly into UserGroupInformation.createProxyUser(...). Since the effective user in RangerDefaultJwtAuthHandler is trimmed, the authorization check should also use a trimmed doAsUser to avoid authorization mismatches for values with surrounding whitespace.

Suggested change
try {
UserGroupInformation ugi = UserGroupInformation.createRemoteUser(realUser);
ugi = UserGroupInformation.createProxyUser(doAsUser, ugi);
ProxyUsers.authorize(ugi, remoteAddr);
LOG.debug("RangerJwtAuthFilter.authorizeProxyUser(): ProxyUsers.authorize SUCCEEDED for realUser=[{}], doAs=[{}]",
realUser, doAsUser);
return true;
} catch (AuthorizationException ex) {
LOG.warn("JWT ProxyUsers.authorize failed for doAs=[{}], realUser=[{}]: {}", doAsUser, realUser, ex.getMessage());
String trimmedDoAsUser = doAsUser == null ? null : doAsUser.trim();
try {
UserGroupInformation ugi = UserGroupInformation.createRemoteUser(realUser);
ugi = UserGroupInformation.createProxyUser(trimmedDoAsUser, ugi);
ProxyUsers.authorize(ugi, remoteAddr);
LOG.debug("RangerJwtAuthFilter.authorizeProxyUser(): ProxyUsers.authorize SUCCEEDED for realUser=[{}], doAs=[{}]",
realUser, trimmedDoAsUser);
return true;
} catch (AuthorizationException ex) {
LOG.warn("JWT ProxyUsers.authorize failed for doAs=[{}], realUser=[{}]: {}", trimmedDoAsUser, realUser, ex.getMessage());

Copilot uses AI. Check for mistakes.
return false;
}
}

private Configuration getProxyuserConfiguration() {
Configuration conf = new Configuration(false);
PropertiesUtil.getPropertiesMap().forEach((k, v) -> {
if (k.startsWith("ranger.proxyuser.")) {
conf.set(k, v);
Comment on lines +147 to +151
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getProxyuserConfiguration() is introduced as private, which forces tests to use reflection and prevents reuse/overrides. Consider making this method protected (similar to RangerKRBAuthenticationFilter#getProxyuserConfiguration) or package-private so it can be unit-tested and extended without reflection.

Copilot uses AI. Check for mistakes.
}
});
return conf;
}

@Override
public void destroy() {
// Empty method
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
*/
package org.apache.ranger.security.web.filter;

import org.apache.hadoop.conf.Configuration;
import org.apache.ranger.authz.handler.RangerAuth;
import org.apache.ranger.common.PropertiesUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
Expand All @@ -38,6 +40,7 @@
import javax.servlet.http.HttpServletRequest;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collection;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
Expand Down Expand Up @@ -120,4 +123,54 @@ public void testDoFilter_leavesAuthenticationNullWhenAuthenticateReturnsNull()

assertNull(SecurityContextHolder.getContext().getAuthentication());
}

@Test
void testIsProxyEnabled_defaultFalse() {
PropertiesUtil.getPropertiesMap().remove("ranger.authentication.allow.trustedproxy");
RangerJwtAuthFilter filter = new RangerJwtAuthFilter();
assertFalse(filter.isProxyEnabled());
}

@Test
void testIsProxyEnabled_trueWhenConfigured() {
PropertiesUtil.getPropertiesMap().put("ranger.authentication.allow.trustedproxy", "true");
RangerJwtAuthFilter filter = new RangerJwtAuthFilter();
assertTrue(filter.isProxyEnabled());
PropertiesUtil.getPropertiesMap().remove("ranger.authentication.allow.trustedproxy");
}

@Test
void testAuthorizeProxyUser_returnsFalseWhenNoProxyConfigLoaded() {
RangerJwtAuthFilter filter = new RangerJwtAuthFilter();
// no proxyuser config loaded into ProxyUsers -> should fail safely
assertFalse(filter.authorizeProxyUser("knoxui", "admin", "10.0.0.1"));
}
Comment on lines +142 to +147
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion depends on the global static state inside Hadoop ProxyUsers (and/or any prior ProxyUsers.refresh... calls) but the test doesn't reset that state. To make the test deterministic, explicitly refresh ProxyUsers with an empty Configuration(false) for the ranger.proxyuser. prefix (or call filter.initialize() after clearing ranger.proxyuser.* properties) before calling authorizeProxyUser(...).

Copilot uses AI. Check for mistakes.

@Test
void testGetProxyuserConfiguration_copiesOnlyProxyuserKeys() throws Exception {
// Arrange: put both proxyuser keys and non-proxyuser keys
PropertiesUtil.getPropertiesMap().put("ranger.proxyuser.knoxui.hosts", "*");
PropertiesUtil.getPropertiesMap().put("ranger.proxyuser.knoxui.groups", "*");
PropertiesUtil.getPropertiesMap().put("ranger.some.other.key", "shouldNotBeCopied");

RangerJwtAuthFilter filter = new RangerJwtAuthFilter();

// Call private getProxyuserConfiguration() via reflection
Method m = RangerJwtAuthFilter.class.getDeclaredMethod("getProxyuserConfiguration");
m.setAccessible(true);

Configuration conf = (Configuration) m.invoke(filter);
Comment on lines +158 to +162
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses reflection to access getProxyuserConfiguration(), which is brittle and tightly couples the test to private implementation details. If getProxyuserConfiguration() is made protected/package-private (as in RangerKRBAuthenticationFilter), the test can call it directly and avoid reflection.

Copilot uses AI. Check for mistakes.

// Assert: proxyuser keys copied
assertEquals("*", conf.get("ranger.proxyuser.knoxui.hosts"));
assertEquals("*", conf.get("ranger.proxyuser.knoxui.groups"));

// Assert: non-proxyuser keys NOT copied
assertNull(conf.get("ranger.some.other.key"));

// Cleanup
PropertiesUtil.getPropertiesMap().remove("ranger.proxyuser.knoxui.hosts");
PropertiesUtil.getPropertiesMap().remove("ranger.proxyuser.knoxui.groups");
PropertiesUtil.getPropertiesMap().remove("ranger.some.other.key");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,21 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collections;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
Expand Down Expand Up @@ -124,4 +129,124 @@ public void testDoFilter_skipsJwtWhenSsoEnabled() throws IOException, ServletExc
verify(jwtFilter, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class), any(FilterChain.class));
verify(chain, times(1)).doFilter(req, res);
}

@Test
void testDoFilter_invokesJwtFilter_whenBearerHeaderPresent() throws Exception {
RangerContextHolder.resetSecurityContext();
SecurityContextHolder.clearContext();
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");

HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
FilterChain chain = Mockito.mock(FilterChain.class);

Mockito.when(req.getHeader("Authorization")).thenReturn("Bearer token");

RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);

RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
setField(wrapper, "rangerJwtAuthFilter", jwt);

wrapper.doFilter(req, res, chain);

verify(jwt, times(1)).doFilter(req, res, chain);
verify(chain, times(1)).doFilter(req, res);
}

@Test
void testDoFilter_skipsJwt_whenAlreadyAuthenticated_evenIfBearerHeaderPresent() throws Exception {
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");

// mark request authenticated
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
"kafka",
"",
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))));

HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
FilterChain chain = Mockito.mock(FilterChain.class);
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
setField(wrapper, "rangerJwtAuthFilter", jwt);

wrapper.doFilter(req, res, chain);

verify(jwt, never()).doFilter(req, res, chain);
verify(chain, times(1)).doFilter(req, res);
}

@Test
void testDoFilter_skipsJwtFilter_whenNoBearerAndNoCookie() throws Exception {
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");

HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
FilterChain chain = Mockito.mock(FilterChain.class);

// no bearer header, no cookies
Mockito.when(req.getHeader("Authorization")).thenReturn(null);
Mockito.when(req.getCookies()).thenReturn(null);

RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
setField(wrapper, "rangerJwtAuthFilter", jwt);

wrapper.doFilter(req, res, chain);

verify(jwt, never()).doFilter(req, res, chain);
verify(chain, times(1)).doFilter(req, res);
}

@Test
void testDoFilter_invokesJwtFilter_whenJwtCookiePresent() throws Exception {
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");

HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
FilterChain chain = Mockito.mock(FilterChain.class);

Mockito.when(req.getHeader("Authorization")).thenReturn(null);
Mockito.when(req.getCookies()).thenReturn(new Cookie[] {new Cookie("hadoop-jwt", "abc")});
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
setField(wrapper, "rangerJwtAuthFilter", jwt);

wrapper.doFilter(req, res, chain);

verify(jwt, times(1)).doFilter(req, res, chain);
verify(chain, times(1)).doFilter(req, res);
}

@Test
void testDoFilter_redirectsToLogin_whenJwtAttemptedButUnauthenticated_andBrowserAgent() throws Exception {
PropertiesUtil.getPropertiesMap().put("ranger.sso.enabled", "false");
System.setProperty("ranger.default.browser-useragents", "Mozilla");

HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
FilterChain chain = Mockito.mock(FilterChain.class);

Mockito.when(req.getHeader("Authorization")).thenReturn("Bearer token");
Mockito.when(req.getHeader("User-Agent")).thenReturn("Mozilla/5.0");

RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
Mockito.doNothing().when(jwt).doFilter(req, res, chain);

RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
wrapper.initialize(); // loads browser agents from properties/system property
setField(wrapper, "rangerJwtAuthFilter", jwt);

wrapper.doFilter(req, res, chain);

verify(res, times(1)).sendRedirect("/login.jsp");
verify(chain, times(1)).doFilter(req, res);
Comment on lines +225 to +244
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.setProperty("ranger.default.browser-useragents", ...) is set in this test but never restored/cleared, which can leak into other tests in the JVM. Save the previous value and restore it in a finally block, or clear/restore it in @AfterEach.

Suggested change
System.setProperty("ranger.default.browser-useragents", "Mozilla");
HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
FilterChain chain = Mockito.mock(FilterChain.class);
Mockito.when(req.getHeader("Authorization")).thenReturn("Bearer token");
Mockito.when(req.getHeader("User-Agent")).thenReturn("Mozilla/5.0");
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
Mockito.doNothing().when(jwt).doFilter(req, res, chain);
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
wrapper.initialize(); // loads browser agents from properties/system property
setField(wrapper, "rangerJwtAuthFilter", jwt);
wrapper.doFilter(req, res, chain);
verify(res, times(1)).sendRedirect("/login.jsp");
verify(chain, times(1)).doFilter(req, res);
String previousBrowserUserAgents = System.getProperty("ranger.default.browser-useragents");
System.setProperty("ranger.default.browser-useragents", "Mozilla");
try {
HttpServletRequest req = Mockito.mock(HttpServletRequest.class);
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
FilterChain chain = Mockito.mock(FilterChain.class);
Mockito.when(req.getHeader("Authorization")).thenReturn("Bearer token");
Mockito.when(req.getHeader("User-Agent")).thenReturn("Mozilla/5.0");
RangerJwtAuthFilter jwt = Mockito.mock(RangerJwtAuthFilter.class);
Mockito.doNothing().when(jwt).doFilter(req, res, chain);
RangerJwtAuthWrapper wrapper = new RangerJwtAuthWrapper();
wrapper.initialize(); // loads browser agents from properties/system property
setField(wrapper, "rangerJwtAuthFilter", jwt);
wrapper.doFilter(req, res, chain);
verify(res, times(1)).sendRedirect("/login.jsp");
verify(chain, times(1)).doFilter(req, res);
} finally {
if (previousBrowserUserAgents == null) {
System.clearProperty("ranger.default.browser-useragents");
} else {
System.setProperty("ranger.default.browser-useragents", previousBrowserUserAgents);
}
}

Copilot uses AI. Check for mistakes.
}

private static void setField(Object target, String fieldName, Object value) throws Exception {
Field f = target.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(target, value);
}
}
Loading