From bcd82af2ec946e32bbaaf7a7544dfafa25aa01e8 Mon Sep 17 00:00:00 2001 From: Vyom Mani Tiwari Date: Fri, 10 Jul 2026 15:25:10 +0530 Subject: [PATCH] RANGER-5677: Plugin REST endpoints silently ignore supplied credentials due to security=none filter bypass; authentication outcome depends on a trailing slash --- .../filter/RangerKRBAuthenticationFilter.java | 32 +++++++ .../conf.dist/security-applicationContext.xml | 10 +- .../TestRangerKRBAuthenticationFilter.java | 93 +++++++++++++++++++ 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerKRBAuthenticationFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerKRBAuthenticationFilter.java index f8bb49312d7..3682a650a18 100644 --- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerKRBAuthenticationFilter.java +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerKRBAuthenticationFilter.java @@ -70,11 +70,13 @@ import java.io.InputStream; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.EventListener; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -106,6 +108,12 @@ public class RangerKRBAuthenticationFilter extends RangerKrbFilter { static final String RULES_MECHANISM_PARAM = "kerberos.name.rules.mechanism"; private static final String KERBEROS_TYPE = "kerberos"; + private static final Set OPTIONAL_AUTH_DOWNLOAD_URL_PREFIXES = new HashSet<>(Arrays.asList( + "/service/gds/download/", + "/service/plugins/policies/download/", + "/service/tags/download/", + "/service/roles/download/", + "/service/xusers/download/")); protected static ServletContext noContext = new ServletContext() { @Override @@ -468,6 +476,15 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha String authtype = PropertiesUtil.getProperty(RANGER_AUTH_TYPE); HttpServletRequest httpRequest = (HttpServletRequest) request; Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication(); + // Optional-auth endpoints must not be forced through a SPNEGO challenge when the client + // presents no credentials at all - let the request proceed as anonymous and leave the + // access decision to the REST layer. A request that does present an Authorization header + // (Negotiate or Basic) falls through to the normal logic below and is validated as usual. + if ((existingAuth == null || !existingAuth.isAuthenticated()) && StringUtils.isEmpty(httpRequest.getHeader("Authorization")) && isOptionalAuthDownloadUrl(httpRequest)) { + LOG.debug("RangerKRBAuthenticationFilter: no credentials presented for optional-auth download URL [{}]; skipping SPNEGO challenge", httpRequest.getRequestURI()); + filterChain.doFilter(request, response); + return; + } if (isSpnegoEnable(authtype) && (existingAuth == null || !existingAuth.isAuthenticated())) { KerberosName.setRules(PropertiesUtil.getProperty(NAME_RULES, "DEFAULT")); @@ -708,6 +725,21 @@ private void handleTimeoutRequest(HttpServletRequest httpRequest, HttpServletRes httpResponse.sendRedirect(redirectUrl); } + private boolean isOptionalAuthDownloadUrl(HttpServletRequest request) { + String uri = request.getRequestURI(); + if (uri == null) { + return false; + } + String ctx = request.getContextPath(); + String path = (ctx != null && !ctx.isEmpty() && uri.startsWith(ctx)) ? uri.substring(ctx.length()) : uri; + for (String prefix : OPTIONAL_AUTH_DOWNLOAD_URL_PREFIXES) { + if (path.startsWith(prefix)) { + return true; + } + } + return false; + } + private boolean isSpnegoEnable(String authType) { String principal = PropertiesUtil.getProperty(PRINCIPAL); String keytabPath = PropertiesUtil.getProperty(KEYTAB); diff --git a/security-admin/src/main/resources/conf.dist/security-applicationContext.xml b/security-admin/src/main/resources/conf.dist/security-applicationContext.xml index bbb94a3a431..66bf07a4901 100644 --- a/security-admin/src/main/resources/conf.dist/security-applicationContext.xml +++ b/security-admin/src/main/resources/conf.dist/security-applicationContext.xml @@ -43,13 +43,8 @@ http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd"> - - - - - @@ -63,6 +58,11 @@ http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd"> + + + + + diff --git a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerKRBAuthenticationFilter.java b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerKRBAuthenticationFilter.java index f9b52929395..df9a8927168 100644 --- a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerKRBAuthenticationFilter.java +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerKRBAuthenticationFilter.java @@ -66,6 +66,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; @@ -473,4 +474,96 @@ public void testProtectedDoFilter_trustedProxy_successSetsAuthAndDelegates() thr assertNotNull(SecurityContextHolder.getContext().getAuthentication()); verify(chain, times(1)).doFilter(req, res); } + + @Test + public void testIsOptionalAuthDownloadUrl_reflection() throws Exception { + RangerKRBAuthenticationFilter filter = new RangerKRBAuthenticationFilter(); + Method m = RangerKRBAuthenticationFilter.class.getDeclaredMethod("isOptionalAuthDownloadUrl", HttpServletRequest.class); + m.setAccessible(true); + // matches: each of the five known prefixes, with and without a trailing slash + String[] matchingUris = { + "/service/gds/download/dev_hive", + "/service/gds/download/dev_hive/", + "/service/plugins/policies/download/dev_hive", + "/service/plugins/policies/download/dev_hive/", + "/service/tags/download/dev_hive", + "/service/roles/download/dev_hive", + "/service/xusers/download/dev_hive" + }; + for (String uri : matchingUris) { + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + when(req.getRequestURI()).thenReturn(uri); + when(req.getContextPath()).thenReturn(""); + boolean result = (Boolean) m.invoke(filter, req); + assertTrue(result, "expected match for URI: " + uri); + } + + // non-match: a similarly-shaped but unrelated URL + HttpServletRequest nonMatching = Mockito.mock(HttpServletRequest.class); + when(nonMatching.getRequestURI()).thenReturn("/service/plugins/secure/policies/download/dev_hive"); + when(nonMatching.getContextPath()).thenReturn(""); + assertEquals(false, m.invoke(filter, nonMatching)); + + // non-match: unrelated path entirely + HttpServletRequest unrelated = Mockito.mock(HttpServletRequest.class); + when(unrelated.getRequestURI()).thenReturn("/service/public/api/repository/dev_hive"); + when(unrelated.getContextPath()).thenReturn(""); + assertEquals(false, m.invoke(filter, unrelated)); + + HttpServletRequest nullUri = Mockito.mock(HttpServletRequest.class); + assertEquals(false, m.invoke(filter, nullUri)); + + // context-path stripping: URI includes a context path prefix that must be stripped + // before matching against the download-URL prefixes + HttpServletRequest withContext = Mockito.mock(HttpServletRequest.class); + when(withContext.getRequestURI()).thenReturn("/ranger/service/tags/download/dev_hive"); + when(withContext.getContextPath()).thenReturn("/ranger"); + assertEquals(true, m.invoke(filter, withContext)); + } + + @Test + public void testDoFilter_optionalAuthDownloadUrl_noAuthHeader_bypassesSpnegoChallenge() throws Exception { + File kt = File.createTempFile("krb", ".keytab"); + kt.deleteOnExit(); + PropertiesUtil.getPropertiesMap().put("hadoop.security.authentication", "kerberos"); + PropertiesUtil.getPropertiesMap().put("ranger.spnego.kerberos.principal", "HTTP/localhost@EXAMPLE.COM"); + PropertiesUtil.getPropertiesMap().put("ranger.spnego.kerberos.keytab", kt.getAbsolutePath()); + RangerKRBAuthenticationFilter filter = new RangerKRBAuthenticationFilter(); + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + FilterChain chain = Mockito.mock(FilterChain.class); + + when(req.getRequestURI()).thenReturn("/service/plugins/policies/download/dev_hive"); + when(req.getContextPath()).thenReturn(""); + when(req.getHeader("Authorization")).thenReturn(null); + filter.doFilter(req, res, chain); + verify(chain, times(1)).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } + + @Test + public void testDoFilter_optionalAuthDownloadUrl_withAuthorizationHeader_doesNotBypass() throws Exception { + File kt = File.createTempFile("krb", ".keytab"); + kt.deleteOnExit(); + PropertiesUtil.getPropertiesMap().put("hadoop.security.authentication", "kerberos"); + PropertiesUtil.getPropertiesMap().put("ranger.spnego.kerberos.principal", "HTTP/localhost@EXAMPLE.COM"); + PropertiesUtil.getPropertiesMap().put("ranger.spnego.kerberos.keytab", kt.getAbsolutePath()); + + RangerKRBAuthenticationFilter filter = new RangerKRBAuthenticationFilter(); + + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + FilterChain chain = Mockito.mock(FilterChain.class); + + when(req.getRequestURI()).thenReturn("/service/plugins/policies/download/dev_hive"); + when(req.getContextPath()).thenReturn(""); + when(req.getHeader("Authorization")).thenReturn("Negotiate abcd1234"); + + try { + filter.doFilter(req, res, chain); + } catch (Exception expected) { + // real SPNEGO negotiation was attempted, as expected, and failed due to this + // minimal test setup not initializing the filter's SPNEGO internals. + } + verify(chain, never()).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + } }