This is an automated email from the ASF dual-hosted git repository. abhi pushed a commit to branch ranger-2.9 in repository https://gitbox.apache.org/repos/asf/ranger.git
commit 1e12062734992e5de38a8b910e0183c31af2d2c9 Author: Abhishek Kumar <[email protected]> AuthorDate: Wed Mar 18 12:25:53 2026 -0700 RANGER-5499: Add support for header based authentication (#873) Co-authored-by: Madhan Neethiraj <[email protected]> Co-authored-by: Cursor AI (assisted) <[email protected]> (cherry picked from commit 2345c1d79d63f632bab237a8b08426d056b30f6c) (cherry picked from commit d40fac0b695007394dab90be0b8384f2c51aea03) --- mkdocs/.gitignore | 1 + .../web/filter/RangerAuthenticationToken.java | 55 +++++ .../web/filter/RangerHeaderPreAuthFilter.java | 117 ++++++++++ .../RangerSecurityContextFormationFilter.java | 49 ++-- .../main/resources/conf.dist/ranger-admin-site.xml | 14 ++ .../conf.dist/security-applicationContext.xml | 4 + .../web/filter/TestRangerHeaderPreAuthFilter.java | 179 +++++++++++++++ .../TestRangerSecurityContextFormationFilter.java | 250 +++++++++++++++++++++ 8 files changed, 653 insertions(+), 16 deletions(-) diff --git a/mkdocs/.gitignore b/mkdocs/.gitignore new file mode 100644 index 000000000..988107fe1 --- /dev/null +++ b/mkdocs/.gitignore @@ -0,0 +1 @@ +.cache/ \ No newline at end of file diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerAuthenticationToken.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerAuthenticationToken.java new file mode 100644 index 000000000..ccf43a417 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerAuthenticationToken.java @@ -0,0 +1,55 @@ +/* + * 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.security.web.filter; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +public class RangerAuthenticationToken extends AbstractAuthenticationToken { + private static final long serialVersionUID = 1L; + + private final Object principal; + private final int authType; + + public RangerAuthenticationToken(UserDetails principal, Collection<? extends GrantedAuthority> authorities, int authType) { + super(authorities); + + this.principal = principal; + this.authType = authType; + + super.setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Object getCredentials() { + return null; + } + + public int getAuthType() { + return authType; + } +} 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 new file mode 100644 index 000000000..89bb78750 --- /dev/null +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java @@ -0,0 +1,117 @@ +/* + * 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.security.web.filter; + +import org.apache.commons.lang3.StringUtils; +import org.apache.ranger.biz.UserMgr; +import org.apache.ranger.common.PropertiesUtil; +import org.apache.ranger.entity.XXAuthSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.web.filter.GenericFilterBean; + +import javax.annotation.PostConstruct; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class RangerHeaderPreAuthFilter extends GenericFilterBean { + private static final Logger LOG = LoggerFactory.getLogger(RangerHeaderPreAuthFilter.class); + + 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_REQUEST_ID_HEADER_NAME = "ranger.admin.authn.header.requestid"; + + private boolean headerAuthEnabled; + private String userNameHeaderName; + + @Autowired + UserMgr userMgr; + + @PostConstruct + public void initialize(FilterConfig filterConfig) throws ServletException { + headerAuthEnabled = PropertiesUtil.getBooleanProperty(PROP_HEADER_AUTH_ENABLED, false); + userNameHeaderName = PropertiesUtil.getProperty(PROP_USERNAME_HEADER_NAME); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + + if (headerAuthEnabled) { + Authentication existingAuthn = SecurityContextHolder.getContext().getAuthentication(); + + if (existingAuthn == null || !existingAuthn.isAuthenticated()) { + String username = StringUtils.trimToNull(httpRequest.getHeader(userNameHeaderName)); + + if (StringUtils.isNotBlank(username)) { + List<GrantedAuthority> grantedAuthorities = getAuthoritiesFromRanger(username); + final UserDetails principal = new User(username, "", grantedAuthorities); + RangerAuthenticationToken authToken = new RangerAuthenticationToken(principal, grantedAuthorities, XXAuthSession.AUTH_TYPE_TRUSTED_PROXY); + + authToken.setDetails(new WebAuthenticationDetails(httpRequest)); + + SecurityContextHolder.getContext().setAuthentication(authToken); + + LOG.debug("Authenticated request using trusted headers for user={}", username); + } else { + LOG.debug("Username header '{}' is missing or empty in the request!", userNameHeaderName); + } + } + } else { + LOG.debug("Header-based authentication is disabled!"); + } + + chain.doFilter(request, response); + } + + /** + * Loads authorities from Ranger DB + */ + private List<GrantedAuthority> getAuthoritiesFromRanger(String username) { + List<GrantedAuthority> ret = new ArrayList<>(); + Collection<String> roleList = userMgr.getRolesByLoginId(username); + + if (roleList != null) { + for (String role : roleList) { + if (StringUtils.isNotBlank(role)) { + ret.add(new SimpleGrantedAuthority(role)); + } + } + } + + return ret; + } +} diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSecurityContextFormationFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSecurityContextFormationFilter.java index 71d7af0d1..65706070f 100644 --- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSecurityContextFormationFilter.java +++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerSecurityContextFormationFilter.java @@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.apache.commons.lang3.StringUtils; import org.apache.ranger.biz.SessionMgr; import org.apache.ranger.biz.XUserMgr; import org.apache.ranger.common.GUIDUtil; @@ -43,6 +44,8 @@ import org.apache.ranger.security.context.RangerContextHolder; import org.apache.ranger.security.context.RangerSecurityContext; import org.apache.ranger.util.RestUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -50,6 +53,7 @@ import org.springframework.web.filter.GenericFilterBean; public class RangerSecurityContextFormationFilter extends GenericFilterBean { + private static final Logger LOG = LoggerFactory.getLogger(RangerSecurityContextFormationFilter.class); public static final String AKA_SC_SESSION_KEY = "AKA_SECURITY_CONTEXT"; public static final String USER_AGENT = "User-Agent"; @@ -65,11 +69,13 @@ public class RangerSecurityContextFormationFilter extends GenericFilterBean { @Autowired GUIDUtil guidUtil; - - String testIP = null; + + private final String testIP; + private final String requestIdHeaderName; public RangerSecurityContextFormationFilter() { - testIP = PropertiesUtil.getProperty("xa.env.ip"); + this.testIP = PropertiesUtil.getProperty("xa.env.ip"); + this.requestIdHeaderName = PropertiesUtil.getProperty(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME); } /* @@ -113,14 +119,14 @@ public void doFilter(ServletRequest request, ServletResponse response, requestContext.setUserAgent(userAgent); requestContext.setDeviceType(httpUtil .getDeviceType(httpRequest)); - requestContext.setServerRequestId(guidUtil.genGUID()); + requestContext.setServerRequestId(getRequestId(auth, httpRequest)); requestContext.setRequestURL(httpRequest.getRequestURI()); requestContext.setClientTimeOffsetInMinute(clientTimeOffset); context.setRequestContext(requestContext); RangerContextHolder.setSecurityContext(context); - int authType = getAuthType(httpRequest); + int authType = getAuthType(auth, httpRequest); UserSessionBase userSession = sessionMgr.processSuccessLogin( authType, userAgent, httpRequest); @@ -159,25 +165,36 @@ private void setupAdminOpContext(ServletRequest request) { } } - private int getAuthType(HttpServletRequest request) { - int authType; + private int getAuthType(Authentication auth, HttpServletRequest request) { + if (auth instanceof RangerAuthenticationToken) { + return ((RangerAuthenticationToken) auth).getAuthType(); + } + Object ssoEnabledObj = request.getAttribute("ssoEnabled"); Boolean ssoEnabled = ssoEnabledObj != null ? Boolean.valueOf(String.valueOf(ssoEnabledObj)) : PropertiesUtil.getBooleanProperty("ranger.sso.enabled", false); if (ssoEnabled) { - authType = XXAuthSession.AUTH_TYPE_SSO; + return XXAuthSession.AUTH_TYPE_SSO; } else if (request.getAttribute("spnegoEnabled") != null && Boolean.valueOf(String.valueOf(request.getAttribute("spnegoEnabled")))){ if (request.getAttribute("trustedProxyEnabled") != null && Boolean.valueOf(String.valueOf(request.getAttribute("trustedProxyEnabled")))) { - if (logger.isDebugEnabled()) { - logger.debug("Setting auth type as trusted proxy"); - } - authType = XXAuthSession.AUTH_TYPE_TRUSTED_PROXY; + LOG.debug("Setting auth type as trusted proxy"); + + return XXAuthSession.AUTH_TYPE_TRUSTED_PROXY; } else { - authType = XXAuthSession.AUTH_TYPE_KERBEROS; + return XXAuthSession.AUTH_TYPE_KERBEROS; } - } else { - authType = XXAuthSession.AUTH_TYPE_PASSWORD; } - return authType; + + return XXAuthSession.AUTH_TYPE_PASSWORD; + } + + private String getRequestId(Authentication auth, HttpServletRequest request) { + String ret = null; + + if (requestIdHeaderName != null && auth instanceof RangerAuthenticationToken && ((RangerAuthenticationToken) auth).getAuthType() == XXAuthSession.AUTH_TYPE_TRUSTED_PROXY) { + ret = StringUtils.trimToNull(request.getHeader(requestIdHeaderName)); + } + + return ret != null ? ret : guidUtil.genGUID(); } } 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 2da6f1c43..d1fccc27d 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 @@ -271,6 +271,20 @@ <value>Mozilla,chrome</value> </property> <!-- SSO Properties Ends--> + <!-- Trusted header auth properties start --> + <property> + <name>ranger.admin.authn.header.enabled</name> + <value>false</value> + </property> + <property> + <name>ranger.admin.authn.header.username</name> + <value>x-awc-username</value> + </property> + <property> + <name>ranger.admin.authn.header.requestid</name> + <value>x-awc-requestid</value> + </property> + <!-- Trusted header auth properties end --> <!-- Kerberos Properties starts--> <property> <name>ranger.admin.kerberos.token.valid.seconds</name> 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 8c0ad7ea6..a1dcacd1a 100644 --- a/security-admin/src/main/resources/conf.dist/security-applicationContext.xml +++ b/security-admin/src/main/resources/conf.dist/security-applicationContext.xml @@ -62,6 +62,7 @@ http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd"> </security:headers> <security:session-management session-fixation-protection="newSession" /> <intercept-url pattern="/**" access="isAuthenticated()"/> + <security:custom-filter position="PRE_AUTH_FILTER" ref="headerPreAuthFilter" /> <custom-filter ref="ssoAuthenticationFilter" after="BASIC_AUTH_FILTER" /> <security:custom-filter ref="rangerJwtAuthWrapper" before="SERVLET_API_SUPPORT_FILTER" /> <security:custom-filter ref="krbAuthenticationFilter" after="SERVLET_API_SUPPORT_FILTER" /> @@ -113,6 +114,9 @@ http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd"> <beans:bean id="mdcFilter" class="org.apache.ranger.security.web.filter.RangerMDCFilter"> </beans:bean> + <beans:bean id="headerPreAuthFilter" class="org.apache.ranger.security.web.filter.RangerHeaderPreAuthFilter"> + </beans:bean> + <beans:bean id="ssoAuthenticationFilter" class="org.apache.ranger.security.web.filter.RangerSSOAuthenticationFilter"> </beans:bean> 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 new file mode 100644 index 000000000..2ed69a6fb --- /dev/null +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java @@ -0,0 +1,179 @@ +/* + * 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.security.web.filter; + +import org.apache.ranger.biz.UserMgr; +import org.apache.ranger.common.PropertiesUtil; +import org.apache.ranger.entity.XXAuthSession; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +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.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@TestMethodOrder(MethodOrderer.MethodName.class) +public class TestRangerHeaderPreAuthFilter { + @BeforeEach + public void setUp() { + SecurityContextHolder.clearContext(); + } + + @AfterEach + public void tearDown() { + SecurityContextHolder.clearContext(); + + PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED); + PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME); + PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME); + } + + @Test + public void testDoFilter_disabled_passesThrough() throws Exception { + RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter(); + UserMgr userMgr = mock(UserMgr.class); + + filter.userMgr = userMgr; + filter.initialize(null); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + verify(userMgr, never()).getRolesByLoginId(anyString()); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @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"); + + RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter(); + UserMgr userMgr = mock(UserMgr.class); + + filter.userMgr = userMgr; + filter.initialize(null); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + // no username header — getHeader returns null by default + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + verify(userMgr, never()).getRolesByLoginId(anyString()); + assertNull(SecurityContextHolder.getContext().getAuthentication()); + } + + @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"); + + RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter(); + UserMgr userMgr = mock(UserMgr.class); + + filter.userMgr = userMgr; + filter.initialize(null); + + when(userMgr.getRolesByLoginId("joeuser")).thenReturn(Arrays.asList("ROLE_SYS_ADMIN", "ROLE_USER")); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + when(request.getHeader("x-awc-username")).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); + assertTrue(auth instanceof RangerAuthenticationToken); + RangerAuthenticationToken rangerAuth = (RangerAuthenticationToken) auth; + assertEquals(XXAuthSession.AUTH_TYPE_TRUSTED_PROXY, rangerAuth.getAuthType()); + assertEquals("joeuser", auth.getName()); + + Collection<?> authorities = auth.getAuthorities(); + assertEquals(2, authorities.size()); + assertTrue(authorities.stream().anyMatch(a -> "ROLE_SYS_ADMIN".equals(a.toString()))); + assertTrue(authorities.stream().anyMatch(a -> "ROLE_USER".equals(a.toString()))); + } + }; + + filter.doFilter(request, response, chain); + } + + @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"); + + RangerHeaderPreAuthFilter filter = new RangerHeaderPreAuthFilter(); + UserMgr userMgr = mock(UserMgr.class); + + filter.userMgr = userMgr; + filter.initialize(null); + + UsernamePasswordAuthenticationToken existingAuth = new UsernamePasswordAuthenticationToken("existing-user", "pwd", Collections.singletonList(new SimpleGrantedAuthority("test-role"))); + + SecurityContextHolder.getContext().setAuthentication(existingAuth); + + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + verify(chain).doFilter(request, response); + verify(userMgr, never()).getRolesByLoginId(anyString()); + assertEquals(existingAuth, SecurityContextHolder.getContext().getAuthentication()); + } +} 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 new file mode 100644 index 000000000..ff9f059bc --- /dev/null +++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerSecurityContextFormationFilter.java @@ -0,0 +1,250 @@ +/* + * 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.security.web.filter; + +import org.apache.ranger.biz.SessionMgr; +import org.apache.ranger.biz.XUserMgr; +import org.apache.ranger.common.GUIDUtil; +import org.apache.ranger.common.HTTPUtil; +import org.apache.ranger.common.PropertiesUtil; +import org.apache.ranger.common.RangerCommonEnums; +import org.apache.ranger.common.UserSessionBase; +import org.apache.ranger.entity.XXAuthSession; +import org.apache.ranger.security.context.RangerContextHolder; +import org.apache.ranger.security.context.RangerSecurityContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @generated by Cursor + * @description <Unit Test for TestRangerSecurityContextFormationFilter class> + */ +@ExtendWith(MockitoExtension.class) +@TestMethodOrder(MethodOrderer.MethodName.class) +public class TestRangerSecurityContextFormationFilter { + @AfterEach + public void tearDown() { + SecurityContextHolder.clearContext(); + RangerContextHolder.resetSecurityContext(); + RangerContextHolder.resetOpContext(); + } + + @Test + public void testDoFilter_setsSecurityHeadersAndCleansContext() throws IOException, ServletException { + RangerSecurityContextFormationFilter filter = new RangerSecurityContextFormationFilter(); + + // mock authenticated user to drive context creation path + GrantedAuthority auth = new SimpleGrantedAuthority("ROLE_USER"); + Authentication authentication = new AnonymousAuthenticationToken("key", "principal", Collections.singletonList(auth)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + FilterChain chain = Mockito.mock(FilterChain.class); + + filter.doFilter(req, res, chain); + + // Verify headers + verify(res).setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); + verify(res).setHeader("X-Frame-Options", "DENY"); + verify(res).setHeader("X-XSS-Protection", "1; mode=block"); + verify(res).setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload"); + verify(res).setHeader("Content-Security-Policy", + "default-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';font-src 'self'"); + verify(res).setHeader("X-Permitted-Cross-Domain-Policies", "none"); + + verify(chain).doFilter(any(ServletRequest.class), any(ServletResponse.class)); + + // filter should clean up thread locals + assertNull(RangerContextHolder.getSecurityContext()); + assertNull(RangerContextHolder.getOpContext()); + } + + @Test + public void testDoFilter_setsCreatePrincipalsIfAbsentFlag() throws Exception { + RangerSecurityContextFormationFilter filter = new RangerSecurityContextFormationFilter(); + + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + + // Set anonymous auth to skip dependency calls but still execute + // setupAdminOpContext + GrantedAuthority ga = new SimpleGrantedAuthority("ROLE_ANON"); + Authentication anon = new AnonymousAuthenticationToken("k", "p", Collections.singletonList(ga)); + SecurityContextHolder.getContext().setAuthentication(anon); + + when(req.getParameter("createPrincipalsIfAbsent")).thenReturn("true"); + + FilterChain chain = new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + Boolean flag = RangerContextHolder.getOpContext() != null + ? RangerContextHolder.getOpContext().getCreatePrincipalsIfAbsent() + : null; + assertEquals(Boolean.TRUE, flag); + } + }; + + filter.doFilter(req, res, chain); + + verify(res, times(1)).setHeader("X-Frame-Options", "DENY"); + } + + @Test + public void testGetAuthType_reflectionVariants() throws Exception { + RangerSecurityContextFormationFilter filter = new RangerSecurityContextFormationFilter(); + Method m = RangerSecurityContextFormationFilter.class.getDeclaredMethod("getAuthType", Authentication.class, HttpServletRequest.class); + m.setAccessible(true); + + List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + UserDetails userDetails = new org.springframework.security.core.userdetails.User("u", "", authorities); + HttpServletRequest emptyRequest = Mockito.mock(HttpServletRequest.class); + + // Header-based trusted proxy — identified via RangerAuthenticationToken, no request attributes needed + assertEquals(XXAuthSession.AUTH_TYPE_TRUSTED_PROXY, + m.invoke(filter, new RangerAuthenticationToken(userDetails, authorities, XXAuthSession.AUTH_TYPE_TRUSTED_PROXY), emptyRequest)); + + // SSO — identified via request attribute (RangerSSOAuthenticationFilter sets UsernamePasswordAuthenticationToken) + HttpServletRequest reqSso = Mockito.mock(HttpServletRequest.class); + Mockito.when(reqSso.getAttribute("ssoEnabled")).thenReturn(true); + assertEquals(XXAuthSession.AUTH_TYPE_SSO, + m.invoke(filter, new org.springframework.security.authentication.UsernamePasswordAuthenticationToken("u", "pwd", authorities), reqSso)); + + // Kerberos — identified via spnegoEnabled attribute + HttpServletRequest reqKrb = Mockito.mock(HttpServletRequest.class); + Mockito.when(reqKrb.getAttribute("ssoEnabled")).thenReturn(false); + Mockito.when(reqKrb.getAttribute("spnegoEnabled")).thenReturn(true); + Mockito.when(reqKrb.getAttribute("trustedProxyEnabled")).thenReturn(false); + assertEquals(XXAuthSession.AUTH_TYPE_KERBEROS, + m.invoke(filter, new org.springframework.security.authentication.UsernamePasswordAuthenticationToken("u", "pwd", authorities), reqKrb)); + + // Kerberos trusted proxy — both spnegoEnabled and trustedProxyEnabled + HttpServletRequest reqKrbTp = Mockito.mock(HttpServletRequest.class); + Mockito.when(reqKrbTp.getAttribute("ssoEnabled")).thenReturn(false); + Mockito.when(reqKrbTp.getAttribute("spnegoEnabled")).thenReturn(true); + Mockito.when(reqKrbTp.getAttribute("trustedProxyEnabled")).thenReturn(true); + assertEquals(XXAuthSession.AUTH_TYPE_TRUSTED_PROXY, + m.invoke(filter, new org.springframework.security.authentication.UsernamePasswordAuthenticationToken("u", "pwd", authorities), reqKrbTp)); + + // Password — no RangerAuthenticationToken, ssoEnabled explicitly false, no Kerberos attributes + HttpServletRequest reqPwd = Mockito.mock(HttpServletRequest.class); + Mockito.when(reqPwd.getAttribute("ssoEnabled")).thenReturn(false); + assertEquals(XXAuthSession.AUTH_TYPE_PASSWORD, + m.invoke(filter, new org.springframework.security.authentication.UsernamePasswordAuthenticationToken("u", "pwd", authorities), reqPwd)); + } + + @Test + public void testDoFilter_authenticated_createsSecurityContextAndUserSession() throws Exception { + PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME, "x-awc-requestid"); + + try { + RangerSecurityContextFormationFilter filter = new RangerSecurityContextFormationFilter(); + + SessionMgr sessionMgr = Mockito.mock(SessionMgr.class); + HTTPUtil httpUtil = Mockito.mock(HTTPUtil.class); + GUIDUtil guidUtil = Mockito.mock(GUIDUtil.class); + XUserMgr xUserMgr = Mockito.mock(XUserMgr.class); + + filter.sessionMgr = sessionMgr; + filter.httpUtil = httpUtil; + filter.guidUtil = guidUtil; + filter.xUserMgr = xUserMgr; + + List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + UserDetails userDetails = new org.springframework.security.core.userdetails.User("user", "", authorities); + RangerAuthenticationToken authentication = new RangerAuthenticationToken(userDetails, authorities, XXAuthSession.AUTH_TYPE_TRUSTED_PROXY); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + HttpSession session = Mockito.mock(HttpSession.class); + + 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.getRequestURI()).thenReturn("/secure"); + Mockito.when(httpUtil.getDeviceType(req)).thenReturn(RangerCommonEnums.DEVICE_BROWSER); + + UserSessionBase userSession = Mockito.mock(UserSessionBase.class); + + Mockito.when(userSession.getClientTimeOffsetInMinute()).thenReturn(0); + Mockito.when(sessionMgr.processSuccessLogin(Mockito.anyInt(), Mockito.anyString(), Mockito.any(HttpServletRequest.class))) + .thenReturn(userSession); + + FilterChain chain = new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) { + RangerSecurityContext ctx = RangerContextHolder.getSecurityContext(); + + assertNotNull(ctx); + assertNotNull(ctx.getRequestContext()); + assertEquals("awc-request-1", ctx.getRequestContext().getServerRequestId()); + assertSame(userSession, ctx.getUserSession()); + } + }; + + filter.doFilter(req, res, chain); + + Mockito.verify(session, Mockito.times(1)).setAttribute(Mockito.eq(RangerSecurityContextFormationFilter.AKA_SC_SESSION_KEY), Mockito.any()); + Mockito.verify(res).setHeader("X-Frame-Options", "DENY"); + Mockito.verify(sessionMgr, Mockito.times(1)).processSuccessLogin(Mockito.anyInt(), Mockito.anyString(), Mockito.any(HttpServletRequest.class)); + Mockito.verify(userSession, Mockito.times(1)).setClientTimeOffsetInMinute(Mockito.anyInt()); + + assertNull(RangerContextHolder.getSecurityContext()); + assertNull(RangerContextHolder.getOpContext()); + } finally { + PropertiesUtil.getPropertiesMap().remove(RangerHeaderPreAuthFilter.PROP_REQUEST_ID_HEADER_NAME); + } + } +}
