diff --git a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerDefaultJwtAuthHandler.java b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerDefaultJwtAuthHandler.java index 94ebc7495a..f317cde905 100644 --- a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerDefaultJwtAuthHandler.java +++ b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerDefaultJwtAuthHandler.java @@ -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; @@ -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"; @@ -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); + } + } + 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; + } } diff --git a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java index 53c5aaae2c..76bb5cd8ae 100644 --- a/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java +++ b/ranger-authn/src/main/java/org/apache/ranger/authz/handler/jwt/RangerJwtAuthHandler.java @@ -106,7 +106,7 @@ public void initialize(final Properties config) throws Exception { public abstract ConfigurableJWTProcessor getJwtProcessor(JWSKeySelector 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()"); } @@ -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 { diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java index a43cc30ced..78e1832e93 100644 --- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerJwtAuthFilter.java @@ -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; @@ -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); } @@ -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()); + return false; + } + } + + private Configuration getProxyuserConfiguration() { + Configuration conf = new Configuration(false); + PropertiesUtil.getPropertiesMap().forEach((k, v) -> { + if (k.startsWith("ranger.proxyuser.")) { + conf.set(k, v); + } + }); + return conf; + } + @Override public void destroy() { // Empty method diff --git a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthFilter.java b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthFilter.java index 276c5e3b9d..50d18ab2e1 100644 --- a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthFilter.java +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthFilter.java @@ -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; @@ -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; @@ -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")); + } + + @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); + + // 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"); + } } diff --git a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthWrapper.java b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthWrapper.java index cbde1b7502..0591703a85 100644 --- a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthWrapper.java +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerJwtAuthWrapper.java @@ -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; @@ -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); + } + + 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); + } }