From b29e27125c1cf5f401d9f83bfdeb7aae29d0a045 Mon Sep 17 00:00:00 2001 From: aditya-gupta36 Date: Mon, 7 Jul 2025 19:03:22 +0530 Subject: [PATCH] Atlas[Backend] Fix for improving logout mechanism in Atlas Backend code base --- .../AtlasKnoxSSOAuthenticationFilter.java | 31 +----- .../apache/atlas/web/filters/HeadersUtil.java | 2 + .../apache/atlas/web/filters/RestUtil.java | 2 +- .../atlas/web/resources/AdminResource.java | 9 ++ .../security/AtlasSecurityCommonConfig.java | 32 ++++++ .../web/security/AtlasSecurityConfig.java | 9 +- .../security/CustomLogoutSuccessHandler.java | 70 ++++++++++++ .../AtlasKnoxSSOAuthenticationFilterTest.java | 105 ++++++++++++++++++ 8 files changed, 229 insertions(+), 31 deletions(-) create mode 100644 webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityCommonConfig.java create mode 100644 webapp/src/main/java/org/apache/atlas/web/security/CustomLogoutSuccessHandler.java create mode 100644 webapp/src/test/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilterTest.java diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java index 85fcbcc6a54..a51ab418c6a 100644 --- a/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java +++ b/webapp/src/main/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilter.java @@ -163,22 +163,12 @@ public void init(FilterConfig filterConfig) throws ServletException { */ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; - AtlasResponseRequestWrapper responseWrapper = new AtlasResponseRequestWrapper(httpResponse); - - HeadersUtil.setSecurityHeaders(responseWrapper); - - if (!ssoEnabled) { - filterChain.doFilter(servletRequest, servletResponse); - - return; - } - HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + httpRequest.setAttribute("ssoEnabled", false); + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + AtlasResponseRequestWrapper responseWrapper = new AtlasResponseRequestWrapper(httpServletResponse); - if (LOG.isDebugEnabled()) { - LOG.debug("Knox doFilter {}", httpRequest.getRequestURI()); - } + HeadersUtil.setSecurityHeaders(responseWrapper); if (httpRequest.getSession() != null && httpRequest.getSession().getAttribute("locallogin") != null) { servletRequest.setAttribute("ssoEnabled", false); @@ -187,19 +177,8 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo return; } - if (jwtProperties == null || isAuthenticated()) { - filterChain.doFilter(servletRequest, servletResponse); - - return; - } - - if (LOG.isDebugEnabled()) { - LOG.debug("Knox ssoEnabled {} {}", ssoEnabled, httpRequest.getRequestURI()); - } - //if jwt properties are loaded and is current not authenticated then it will go for sso authentication //Note : Need to remove !isAuthenticated() after knoxsso solve the bug from cross-origin script - HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; String serializedJWT = getJWTFromCookie(httpRequest); // if we get the hadoop-jwt token from the cookies then will process it further @@ -308,7 +287,7 @@ protected String getJWTFromCookie(HttpServletRequest req) { for (Cookie cookie : cookies) { if (cookieName.equals(cookie.getName())) { LOG.debug("{} cookie has been found and is being processed", cookieName); - + req.setAttribute("ssoEnabled", true); serializedJWT = cookie.getValue(); break; } diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/HeadersUtil.java b/webapp/src/main/java/org/apache/atlas/web/filters/HeadersUtil.java index dbec3cdbfab..ef9614bb451 100644 --- a/webapp/src/main/java/org/apache/atlas/web/filters/HeadersUtil.java +++ b/webapp/src/main/java/org/apache/atlas/web/filters/HeadersUtil.java @@ -50,6 +50,8 @@ public class HeadersUtil { public static final String X_REQUESTED_WITH_VALUE = "XMLHttpRequest"; public static final int SC_AUTHENTICATION_TIMEOUT = 419; public static final String CONFIG_PREFIX_HTTP_RESPONSE_HEADER = "atlas.headers"; + public static final String CACHE_CONTROL = "Cache-Control"; + public static final String CACHE_CONTROL_VAL = "no-cache"; private static final Map HEADER_MAP = new HashMap<>(); diff --git a/webapp/src/main/java/org/apache/atlas/web/filters/RestUtil.java b/webapp/src/main/java/org/apache/atlas/web/filters/RestUtil.java index 0c89a3ffbe0..612fd9a08eb 100644 --- a/webapp/src/main/java/org/apache/atlas/web/filters/RestUtil.java +++ b/webapp/src/main/java/org/apache/atlas/web/filters/RestUtil.java @@ -30,7 +30,7 @@ public class RestUtil { private static final Logger LOG = LoggerFactory.getLogger(RestUtil.class); public static final String TIMEOUT_ACTION = "timeout"; - public static final String LOGOUT_URL = "/logout.html"; + public static final String LOGOUT_URL = "/logout"; public static final String DELIMITTER = "://"; private static final String PROXY_ATLAS_URL_PATH = "/atlas"; diff --git a/webapp/src/main/java/org/apache/atlas/web/resources/AdminResource.java b/webapp/src/main/java/org/apache/atlas/web/resources/AdminResource.java index 06ff0533967..1fe8d264e13 100755 --- a/webapp/src/main/java/org/apache/atlas/web/resources/AdminResource.java +++ b/webapp/src/main/java/org/apache/atlas/web/resources/AdminResource.java @@ -1098,6 +1098,15 @@ public Response serviceReadiness() throws AtlasBaseException { } } + @GET + @Path("/checksso") + @Produces(MediaType.TEXT_PLAIN) + public String checkSSO(@Context HttpServletRequest httpServletRequest) { + Object ssoFlag = httpServletRequest.getAttribute("ssoEnabled"); + LOG.debug("SSO attribute Value: {}", ssoFlag); + return String.valueOf(ssoFlag); + } + private void updateCriteriaWithDefaultValues(AuditReductionCriteria auditReductionCriteria) { if (auditReductionCriteria.getDefaultAgeoutTTLInDays() <= 0) { auditReductionCriteria.setDefaultAgeoutTTLInDays(AtlasConfiguration.ATLAS_AUDIT_DEFAULT_AGEOUT_TTL.getInt()); diff --git a/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityCommonConfig.java b/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityCommonConfig.java new file mode 100644 index 00000000000..80cde70db02 --- /dev/null +++ b/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityCommonConfig.java @@ -0,0 +1,32 @@ +/* + * 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.atlas.web.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AtlasSecurityCommonConfig { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java b/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java index 3d81e9917e1..ddbae263f59 100644 --- a/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java +++ b/webapp/src/main/java/org/apache/atlas/web/security/AtlasSecurityConfig.java @@ -103,6 +103,7 @@ public class AtlasSecurityConfig extends WebSecurityConfigurerAdapter { private final StaleTransactionCleanupFilter staleTransactionCleanupFilter; private final ActiveServerFilter activeServerFilter; private final boolean keycloakEnabled; + private final CustomLogoutSuccessHandler customLogoutSuccessHandler; @Value("${keycloak.configurationFile:WEB-INF/keycloak.json}") private Resource keycloakConfigFileResource; @@ -120,7 +121,7 @@ public AtlasSecurityConfig(AtlasKnoxSSOAuthenticationFilter ssoAuthenticationFil AtlasAuthenticationEntryPoint atlasAuthenticationEntryPoint, Configuration configuration, StaleTransactionCleanupFilter staleTransactionCleanupFilter, - ActiveServerFilter activeServerFilter) { + ActiveServerFilter activeServerFilter, CustomLogoutSuccessHandler customLogoutSuccessHandler) { this.ssoAuthenticationFilter = ssoAuthenticationFilter; this.csrfPreventionFilter = atlasCSRFPreventionFilter; this.atlasAuthenticationFilter = atlasAuthenticationFilter; @@ -131,6 +132,7 @@ public AtlasSecurityConfig(AtlasKnoxSSOAuthenticationFilter ssoAuthenticationFil this.configuration = configuration; this.staleTransactionCleanupFilter = staleTransactionCleanupFilter; this.activeServerFilter = activeServerFilter; + this.customLogoutSuccessHandler = customLogoutSuccessHandler; this.keycloakEnabled = configuration.getBoolean(AtlasAuthenticationProvider.KEYCLOAK_AUTH_METHOD, false); } @@ -220,10 +222,9 @@ protected void configure(HttpSecurity httpSecurity) throws Exception { .passwordParameter("j_password") .and() .logout() - .logoutSuccessUrl("/login.jsp") + .logoutSuccessHandler(customLogoutSuccessHandler) .deleteCookies("ATLASSESSIONID") - .logoutUrl("/logout.html"); - + .logoutUrl("/logout"); //@formatter:on boolean configMigrationEnabled = !StringUtils.isEmpty(configuration.getString(ATLAS_MIGRATION_MODE_FILENAME)); diff --git a/webapp/src/main/java/org/apache/atlas/web/security/CustomLogoutSuccessHandler.java b/webapp/src/main/java/org/apache/atlas/web/security/CustomLogoutSuccessHandler.java new file mode 100644 index 00000000000..b5171796909 --- /dev/null +++ b/webapp/src/main/java/org/apache/atlas/web/security/CustomLogoutSuccessHandler.java @@ -0,0 +1,70 @@ +/* + * 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.atlas.web.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.atlas.web.filters.HeadersUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Component +public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler { + private final ObjectMapper mapper; + private static final Logger LOG = LoggerFactory.getLogger(CustomLogoutSuccessHandler.class); + + @Inject + public CustomLogoutSuccessHandler(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + request.getServletContext().removeAttribute(request.getRequestedSessionId()); + response.setContentType("application/json;charset=UTF-8"); + response.setHeader(HeadersUtil.CACHE_CONTROL, HeadersUtil.CACHE_CONTROL_VAL); + response.setHeader(HeadersUtil.X_FRAME_OPTIONS_KEY, HeadersUtil.X_FRAME_OPTIONS_VAL); + + try { + Map responseMap = new HashMap<>(); + responseMap.put("statusCode", HttpServletResponse.SC_OK); + responseMap.put("msgDesc", "Logout Successful"); + String jsonStr = mapper.writeValueAsString(responseMap); + + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write(jsonStr); + + LOG.debug("Log-out Successfully done. Returning Json : {}", jsonStr); + } catch (IOException e) { + LOG.debug("Error while writing JSON in HttpServletResponse"); + } + } +} diff --git a/webapp/src/test/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilterTest.java b/webapp/src/test/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilterTest.java new file mode 100644 index 00000000000..822e3e01ba5 --- /dev/null +++ b/webapp/src/test/java/org/apache/atlas/web/filters/AtlasKnoxSSOAuthenticationFilterTest.java @@ -0,0 +1,105 @@ +/* + * 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.atlas.web.filters; + +import org.apache.atlas.web.resources.AdminResource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; + +import java.io.IOException; +import java.util.Arrays; + +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +public class AtlasKnoxSSOAuthenticationFilterTest { + @Mock + private FilterChain filterChain; + + @InjectMocks + private AtlasKnoxSSOAuthenticationFilter filter; + + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private MockHttpSession session; + + @InjectMocks + AdminResource adminResource; + + @BeforeMethod + public void testSetup() { + MockitoAnnotations.openMocks(this); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + session = new MockHttpSession(); + request.setSession(session); + + request.setRequestURI("/api/atlas/admin/checksso"); + request.addHeader("User-Agent", "Chrome"); + } + + @Test + public void testDoFilter_withoutJwt_setsSsoDisabled() throws IOException, ServletException { + request.setCookies(); // clear cookies for this test + filter.doFilter(request, response, filterChain); + + assertNull(filter.getJWTFromCookie(request)); + + verify(filterChain).doFilter(request, response); + } + + @Test + public void testDoFilter_withJwt_setsSsoEnabledTrue() throws IOException, ServletException { + request.setCookies(new Cookie("hadoop-jwt", "dummy-jwt-token")); + filter.doFilter(request, response, filterChain); + + assertNotNull(filter.getJWTFromCookie(request)); + assertNotNull(request.getCookies()); + assertTrue(Arrays.stream(request.getCookies()) + .anyMatch(cookie -> cookie.getValue().equals("dummy-jwt-token"))); + + verify(filterChain).doFilter(request, response); + } + + @Test + public void test_CheckSSO_API_true() { + request.setAttribute("ssoEnabled", true); + String result = adminResource.checkSSO(request); + assertEquals(result, "true"); + } + + @Test + public void test_CheckSSO_API_false() { + request.setAttribute("ssoEnabled", false); + String result = adminResource.checkSSO(request); + assertEquals(result, "false"); + } +}