vyommani commented on code in PR #1200:
URL: https://github.com/apache/ranger/pull/1200#discussion_r3964481092


##########
security-admin/src/test/java/org/apache/ranger/biz/TestSessionMgr.java:
##########
@@ -730,4 +737,372 @@ public void 
testSetUserRoles_ConfigSuperUserGrantsKeyAdminForSysAdmin() {
 
         
PropertiesUtil.getPropertiesMap().remove(RangerConstants.RANGER_ADMIN_SUPER_USERS);
     }
+
+    @Test
+    public void testProcessSuccessLogin_LimitZeroDoesNotExpireOldestSession() {
+        RangerContextHolder.setSecurityContext(null);
+        
PropertiesUtil.getPropertiesMap().put(SessionMgr.PROP_SESSION_LIMIT_CONCURRENCY,
 "0");
+
+        setupAuthentication("limitUser");
+
+        XXPortalUser portalUser = portalUser("limitUser", 70L);
+        stubPortalUserLookup(portalUser);
+        stubRolesAndPermissions(portalUser);
+        stubAuthSessionCreate(200L);
+        
when(httpUtil.getDeviceType(anyString())).thenReturn(RangerCommonEnums.DEVICE_UNKNOWN);
+
+        HttpSession currentSession = mock(HttpSession.class);
+        when(currentSession.getAttribute("auditLoginId")).thenReturn(null);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getSession()).thenReturn(currentSession);
+        when(request.getRequestURI()).thenReturn("/index.html");
+        when(request.getAttribute("spnegoEnabled")).thenReturn(null);
+
+        UserSessionBase ret = 
sessionMgr.processSuccessLogin(XXAuthSession.AUTH_TYPE_PASSWORD, "Mozilla/5.0", 
request);
+
+        assertNotNull(ret);
+        assertEquals(70L, ret.getUserId());
+    }
+
+    @Test
+    public void 
testProcessSuccessLogin_LimitOneExpiresOldestFormLoginSession() {
+        RangerContextHolder.setSecurityContext(null);
+        
PropertiesUtil.getPropertiesMap().put(SessionMgr.PROP_SESSION_LIMIT_CONCURRENCY,
 "1");
+
+        setupAuthentication("limitUser");
+
+        XXPortalUser portalUser = portalUser("limitUser", 71L);
+        stubPortalUserLookup(portalUser);
+        stubRolesAndPermissions(portalUser);
+        stubAuthSessionCreate(201L);
+        
when(httpUtil.getDeviceType(anyString())).thenReturn(RangerCommonEnums.DEVICE_UNKNOWN);
+
+        HttpSession currentSession = mock(HttpSession.class);
+        when(currentSession.getAttribute("auditLoginId")).thenReturn(null);
+
+        HttpSession oldestSession = mockUiSession("limitUser", 71L, false, 1L);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getSession()).thenReturn(currentSession);
+        when(request.getRequestURI()).thenReturn("/index.html");
+        when(request.getAttribute("spnegoEnabled")).thenReturn(null);
+
+        try (MockedStatic<RangerHttpSessionListener> mocked = 
Mockito.mockStatic(RangerHttpSessionListener.class)) {
+            CopyOnWriteArrayList<HttpSession> sessions = new 
CopyOnWriteArrayList<>();
+            sessions.add(oldestSession);
+            
mocked.when(RangerHttpSessionListener::getActiveSessionOnServer).thenReturn(sessions);
+
+            UserSessionBase ret = 
sessionMgr.processSuccessLogin(XXAuthSession.AUTH_TYPE_PASSWORD, "Mozilla/5.0", 
request);
+
+            assertNotNull(ret);
+            
verify(oldestSession).setAttribute(SessionMgr.SESSION_ATTR_CONCURRENT_EXPIRED, 
Boolean.TRUE);
+            
verify(oldestSession).setAttribute(SessionMgr.SESSION_ATTR_CONCURRENT_EXPIRED_SSO,
 false);
+            verify(oldestSession).invalidate();
+        }
+    }
+
+    @Test
+    public void 
testProcessSuccessLogin_LimitOneMarksOldestSsoSessionExpiredWithoutInvalidate() 
{
+        RangerContextHolder.setSecurityContext(null);
+        
PropertiesUtil.getPropertiesMap().put(SessionMgr.PROP_SESSION_LIMIT_CONCURRENCY,
 "1");
+
+        setupAuthentication("ssoUser");
+
+        XXPortalUser portalUser = portalUser("ssoUser", 72L);
+        stubPortalUserLookup(portalUser);
+        stubRolesAndPermissions(portalUser);
+        stubAuthSessionCreate(202L);
+        
when(httpUtil.getDeviceType(anyString())).thenReturn(RangerCommonEnums.DEVICE_UNKNOWN);
+
+        HttpSession currentSession = mock(HttpSession.class);
+        when(currentSession.getAttribute("auditLoginId")).thenReturn(null);
+
+        HttpSession oldestSession = mockUiSession("ssoUser", 72L, true, 1L);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getSession()).thenReturn(currentSession);
+        when(request.getRequestURI()).thenReturn("/index.html");
+        when(request.getAttribute("spnegoEnabled")).thenReturn(Boolean.TRUE);
+
+        try (MockedStatic<RangerHttpSessionListener> mocked = 
Mockito.mockStatic(RangerHttpSessionListener.class)) {
+            CopyOnWriteArrayList<HttpSession> sessions = new 
CopyOnWriteArrayList<>();
+            sessions.add(oldestSession);
+            
mocked.when(RangerHttpSessionListener::getActiveSessionOnServer).thenReturn(sessions);
+
+            UserSessionBase ret = 
sessionMgr.processSuccessLogin(XXAuthSession.AUTH_TYPE_TRUSTED_PROXY, 
"Mozilla/5.0", request);
+
+            assertNotNull(ret);
+            assertTrue(ret.isSSOEnabled());
+            
verify(oldestSession).setAttribute(SessionMgr.SESSION_ATTR_CONCURRENT_EXPIRED, 
Boolean.TRUE);
+            
verify(oldestSession).setAttribute(SessionMgr.SESSION_ATTR_CONCURRENT_EXPIRED_SSO,
 true);
+            verify(oldestSession, never()).invalidate();
+        }
+    }
+
+    @Test
+    public void 
testProcessSuccessLogin_DownloadRequestDoesNotConsumeSessionQuota() {
+        RangerContextHolder.setSecurityContext(null);
+        
PropertiesUtil.getPropertiesMap().put(SessionMgr.PROP_SESSION_LIMIT_CONCURRENCY,
 "1");
+
+        setupAuthentication("limitUser");
+
+        XXPortalUser portalUser = portalUser("limitUser", 73L);
+        stubPortalUserLookup(portalUser);
+        stubRolesAndPermissions(portalUser);
+        
when(httpUtil.getDeviceType(anyString())).thenReturn(RangerCommonEnums.DEVICE_UNKNOWN);
+
+        HttpSession currentSession = mock(HttpSession.class);
+        when(currentSession.getAttribute("auditLoginId")).thenReturn(null);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getSession()).thenReturn(currentSession);
+        
when(request.getRequestURI()).thenReturn("/service/plugins/policies/download/hadoopdev");
+        when(request.getAttribute("spnegoEnabled")).thenReturn(null);
+
+        UserSessionBase ret = 
sessionMgr.processSuccessLogin(XXAuthSession.AUTH_TYPE_PASSWORD, "Mozilla/5.0", 
request);
+
+        assertNotNull(ret);
+        
verify(currentSession).setAttribute(SessionMgr.SESSION_ATTR_DOWNLOAD_ONLY, 
Boolean.TRUE);
+    }
+
+    @Test
+    public void testProcessSuccessLogin_ApiRequestDoesNotConsumeSessionQuota() 
{
+        RangerContextHolder.setSecurityContext(null);
+        
PropertiesUtil.getPropertiesMap().put(SessionMgr.PROP_SESSION_LIMIT_CONCURRENCY,
 "1");
+
+        setupAuthentication("limitUser");
+
+        XXPortalUser portalUser = portalUser("limitUser", 74L);
+        stubPortalUserLookup(portalUser);
+        stubRolesAndPermissions(portalUser);
+        stubAuthSessionCreate(204L);
+        
when(httpUtil.getDeviceType(anyString())).thenReturn(RangerCommonEnums.DEVICE_UNKNOWN);
+
+        HttpSession currentSession = mock(HttpSession.class);
+        when(currentSession.getAttribute("auditLoginId")).thenReturn(null);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getSession()).thenReturn(currentSession);
+        
when(request.getRequestURI()).thenReturn("/service/public/v2/api/policies");
+        when(request.getAttribute("spnegoEnabled")).thenReturn(null);
+
+        UserSessionBase ret = 
sessionMgr.processSuccessLogin(XXAuthSession.AUTH_TYPE_PASSWORD, "curl/8.0", 
request);
+
+        assertNotNull(ret);
+        verify(currentSession).setAttribute(SessionMgr.SESSION_ATTR_NON_UI, 
Boolean.TRUE);
+        verify(currentSession, 
never()).setAttribute(eq(SessionMgr.SESSION_ATTR_CONCURRENT_EXPIRED), any());
+    }
+
+    @Test
+    public void 
testProcessSuccessLogin_LimitTwoExpiresOnlyOldestOfTwoExistingSessions() {
+        RangerContextHolder.setSecurityContext(null);
+        
PropertiesUtil.getPropertiesMap().put(SessionMgr.PROP_SESSION_LIMIT_CONCURRENCY,
 "2");
+
+        setupAuthentication("limitUser");
+
+        XXPortalUser portalUser = portalUser("limitUser", 75L);
+        stubPortalUserLookup(portalUser);
+        stubRolesAndPermissions(portalUser);
+        stubAuthSessionCreate(205L);
+        
when(httpUtil.getDeviceType(anyString())).thenReturn(RangerCommonEnums.DEVICE_UNKNOWN);
+
+        HttpSession currentSession = mock(HttpSession.class);
+        when(currentSession.getAttribute("auditLoginId")).thenReturn(null);
+
+        HttpSession oldestSession = mockUiSession("limitUser", 75L, false, 1L);
+        HttpSession newerSession  = mockUiSession("limitUser", 75L, false, 2L);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getSession()).thenReturn(currentSession);
+        when(request.getRequestURI()).thenReturn("/index.html");
+        when(request.getAttribute("spnegoEnabled")).thenReturn(null);
+
+        try (MockedStatic<RangerHttpSessionListener> mocked = 
Mockito.mockStatic(RangerHttpSessionListener.class)) {
+            CopyOnWriteArrayList<HttpSession> sessions = new 
CopyOnWriteArrayList<>();
+            sessions.add(newerSession);
+            sessions.add(oldestSession);
+            
mocked.when(RangerHttpSessionListener::getActiveSessionOnServer).thenReturn(sessions);
+
+            UserSessionBase ret = 
sessionMgr.processSuccessLogin(XXAuthSession.AUTH_TYPE_PASSWORD, "Mozilla/5.0", 
request);
+
+            assertNotNull(ret);
+            
verify(oldestSession).setAttribute(SessionMgr.SESSION_ATTR_CONCURRENT_EXPIRED, 
Boolean.TRUE);
+            verify(oldestSession).invalidate();
+            verify(newerSession, 
never()).setAttribute(eq(SessionMgr.SESSION_ATTR_CONCURRENT_EXPIRED), any());
+            verify(newerSession, never()).invalidate();
+        }
+    }
+
+    @Test
+    public void testProcessSuccessLogin_DoesNotExpireOtherUsersSessions() {
+        RangerContextHolder.setSecurityContext(null);
+        
PropertiesUtil.getPropertiesMap().put(SessionMgr.PROP_SESSION_LIMIT_CONCURRENCY,
 "1");
+
+        setupAuthentication("userB");
+
+        XXPortalUser portalUser = portalUser("userB", 76L);
+        stubPortalUserLookup(portalUser);
+        stubRolesAndPermissions(portalUser);
+        stubAuthSessionCreate(206L);
+        
when(httpUtil.getDeviceType(anyString())).thenReturn(RangerCommonEnums.DEVICE_UNKNOWN);
+
+        HttpSession currentSession = mock(HttpSession.class);
+        when(currentSession.getAttribute("auditLoginId")).thenReturn(null);
+
+        HttpSession otherUserSession = mockUiSession("userA", 77L, false, 1L);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getSession()).thenReturn(currentSession);
+        when(request.getRequestURI()).thenReturn("/index.html");
+        when(request.getAttribute("spnegoEnabled")).thenReturn(null);
+
+        try (MockedStatic<RangerHttpSessionListener> mocked = 
Mockito.mockStatic(RangerHttpSessionListener.class)) {
+            CopyOnWriteArrayList<HttpSession> sessions = new 
CopyOnWriteArrayList<>();
+            sessions.add(otherUserSession);
+            
mocked.when(RangerHttpSessionListener::getActiveSessionOnServer).thenReturn(sessions);
+
+            UserSessionBase ret = 
sessionMgr.processSuccessLogin(XXAuthSession.AUTH_TYPE_PASSWORD, "Mozilla/5.0", 
request);
+
+            assertNotNull(ret);
+            assertEquals(76L, ret.getUserId());
+            verify(otherUserSession, 
never()).setAttribute(eq(SessionMgr.SESSION_ATTR_CONCURRENT_EXPIRED), any());
+            verify(otherUserSession, never()).invalidate();
+        }
+    }
+
+    @Test
+    public void 
testEnforceConcurrentSessionLimit_ConcurrentSameUserExpiresOldest() throws 
Exception {

Review Comment:
   Nice to see a real multi-threaded test here. One issue with what it actually 
proves: `mockUiSession()` (line 1090) stubs `getAttribute(anyString())` to 
always return null except for `AKA_SC_SESSION_KEY` — it doesn't reflect state 
written by `setAttribute()`. So when thread A calls 
`expireConcurrentSession(oldestSession)` and sets 
`SESSION_ATTR_CONCURRENT_EXPIRED`, thread B's subsequent 
`findActiveUiSessionsForUser()` call still sees 
`getAttribute(SESSION_ATTR_CONCURRENT_EXPIRED)` as null/false and will *also* 
treat `oldestSession` as eligible and expire it again. That's why the assertion 
is `atLeastOnce()` rather than `times(1)` — and it means this test would pass 
identically even if the `synchronized (lock)` block around this method were 
removed, since nothing here actually distinguishes "properly serialized, thread 
B found nothing left to expire" from "unsynchronized, thread B redundantly 
expired the same session again."
   
   Could this be tightened to actually demonstrate the lock matters — e.g. two 
*distinct* candidate sessions (`oldestSession`, `secondOldestSession`) with 
`limit=1`, and asserting that only one of the two ends up expired in total 
across both threads (rather than both, which is what you'd see without the 
lock)? A stateful fake/spy that tracks `setAttribute` calls instead of the 
current stub would also let `otherSessions.size() < limit` actually 
short-circuit correctly on the second thread.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to