From 5b50b90b226046a9fdc9b27cdfad32b45e916c7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 11:30:30 +0000 Subject: [PATCH 1/4] Initial plan From 6742faceb603d5411095315c442121fed0efed7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 11:48:10 +0000 Subject: [PATCH 2/4] Add agent session duration tracking to dashboard graphs Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> --- .../sso/controller/SessionController.java | 11 + .../ActiveWebSocketSessionManager.java | 71 +++++- .../ActiveWebSocketSessionManagerTest.java | 145 ++++++++++++ .../sso/core/services/SessionService.java | 63 ++++++ .../sso/core/services/SessionServiceTest.java | 214 ++++++++++++++++++ 5 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 agent-proxy/src/test/java/io/sentrius/sso/service/ActiveWebSocketSessionManagerTest.java create mode 100644 dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java diff --git a/agent-proxy/src/main/java/io/sentrius/sso/controller/SessionController.java b/agent-proxy/src/main/java/io/sentrius/sso/controller/SessionController.java index 5a4bcddc..093ce795 100644 --- a/agent-proxy/src/main/java/io/sentrius/sso/controller/SessionController.java +++ b/agent-proxy/src/main/java/io/sentrius/sso/controller/SessionController.java @@ -1,6 +1,7 @@ package io.sentrius.sso.controller; import java.util.List; +import java.util.Map; import io.sentrius.sso.core.dto.TerminalLogDTO; import io.sentrius.sso.service.ActiveWebSocketSessionManager; import org.springframework.http.HttpHeaders; @@ -27,4 +28,14 @@ public List listSessions() { return activeWebSocketSessionManager.getActiveSessions(); } + @GetMapping("/agent/durations") + public List> getAgentSessionDurations() { + return activeWebSocketSessionManager.getAgentSessionDurations(); + } + + @GetMapping("/agent/active-durations") + public List> getActiveAgentSessionDurations() { + return activeWebSocketSessionManager.getActiveAgentSessionDurations(); + } + } \ No newline at end of file diff --git a/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java b/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java index e52e078d..162cbc02 100644 --- a/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java +++ b/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java @@ -1,6 +1,10 @@ package io.sentrius.sso.service; import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -13,13 +17,37 @@ @Component public class ActiveWebSocketSessionManager { private final Map sessions = new ConcurrentHashMap<>(); + private final Map sessionStartTimes = new ConcurrentHashMap<>(); + private final List> completedAgentSessions = new ArrayList<>(); public void register(String sessionId, WebSocketSession session) { sessions.put(sessionId, session); + sessionStartTimes.put(sessionId, new Timestamp(System.currentTimeMillis())); } public void unregister(String sessionId) { - sessions.remove(sessionId); + WebSocketSession session = sessions.remove(sessionId); + Timestamp startTime = sessionStartTimes.remove(sessionId); + + if (startTime != null) { + // Calculate duration and store completed session + Timestamp endTime = new Timestamp(System.currentTimeMillis()); + long durationMinutes = ChronoUnit.MINUTES.between( + startTime.toLocalDateTime(), + endTime.toLocalDateTime() + ); + + Map completedSession = new HashMap<>(); + completedSession.put("sessionId", sessionId); + completedSession.put("startTime", startTime); + completedSession.put("endTime", endTime); + completedSession.put("durationMinutes", durationMinutes); + completedSession.put("sessionType", "agent"); + + synchronized (completedAgentSessions) { + completedAgentSessions.add(completedSession); + } + } } public WebSocketSession get(String sessionId) { @@ -37,4 +65,45 @@ public List getActiveSessions() { .build()) .collect(Collectors.toList()); } + + /** + * Get session duration data for agent sessions + * @return List of session duration data + */ + public List> getAgentSessionDurations() { + synchronized (completedAgentSessions) { + return new ArrayList<>(completedAgentSessions); + } + } + + /** + * Get current active agent session durations (for sessions still in progress) + * @return List of active session duration data + */ + public List> getActiveAgentSessionDurations() { + List> activeDurations = new ArrayList<>(); + + for (Map.Entry entry : sessionStartTimes.entrySet()) { + String sessionId = entry.getKey(); + Timestamp startTime = entry.getValue(); + + if (sessions.containsKey(sessionId)) { + long durationMinutes = ChronoUnit.MINUTES.between( + startTime.toLocalDateTime(), + LocalDateTime.now() + ); + + Map activeSession = new HashMap<>(); + activeSession.put("sessionId", sessionId); + activeSession.put("startTime", startTime); + activeSession.put("durationMinutes", durationMinutes); + activeSession.put("sessionType", "agent"); + activeSession.put("active", true); + + activeDurations.add(activeSession); + } + } + + return activeDurations; + } } diff --git a/agent-proxy/src/test/java/io/sentrius/sso/service/ActiveWebSocketSessionManagerTest.java b/agent-proxy/src/test/java/io/sentrius/sso/service/ActiveWebSocketSessionManagerTest.java new file mode 100644 index 00000000..7394a942 --- /dev/null +++ b/agent-proxy/src/test/java/io/sentrius/sso/service/ActiveWebSocketSessionManagerTest.java @@ -0,0 +1,145 @@ +package io.sentrius.sso.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.web.reactive.socket.WebSocketSession; +import org.springframework.web.reactive.socket.HandshakeInfo; + +import java.net.InetSocketAddress; +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ActiveWebSocketSessionManagerTest { + + @Mock + private WebSocketSession webSocketSession; + + @Mock + private HandshakeInfo handshakeInfo; + + private ActiveWebSocketSessionManager sessionManager; + + @BeforeEach + void setUp() { + sessionManager = new ActiveWebSocketSessionManager(); + } + + @Test + void testRegisterAndUnregisterSession() { + // Given + String sessionId = "test-session-1"; + when(webSocketSession.getId()).thenReturn(sessionId); + when(webSocketSession.isOpen()).thenReturn(true); + when(webSocketSession.getHandshakeInfo()).thenReturn(handshakeInfo); + when(handshakeInfo.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 8080)); + + // When - register session + sessionManager.register(sessionId, webSocketSession); + + // Then - session should be active + assertEquals(webSocketSession, sessionManager.get(sessionId)); + assertEquals(1, sessionManager.getActiveSessions().size()); + assertEquals(0, sessionManager.getAgentSessionDurations().size()); + + // When - unregister session + sessionManager.unregister(sessionId); + + // Then - session should be removed and duration recorded + assertNull(sessionManager.get(sessionId)); + assertEquals(0, sessionManager.getActiveSessions().size()); + assertEquals(1, sessionManager.getAgentSessionDurations().size()); + + // Verify session duration data + List> completedSessions = sessionManager.getAgentSessionDurations(); + Map sessionData = completedSessions.get(0); + assertEquals(sessionId, sessionData.get("sessionId")); + assertEquals("agent", sessionData.get("sessionType")); + assertNotNull(sessionData.get("startTime")); + assertNotNull(sessionData.get("endTime")); + assertNotNull(sessionData.get("durationMinutes")); + assertTrue((Long) sessionData.get("durationMinutes") >= 0); + } + + @Test + void testGetActiveAgentSessionDurations() throws InterruptedException { + // Given + String sessionId = "active-session-1"; + when(webSocketSession.getId()).thenReturn(sessionId); + when(webSocketSession.isOpen()).thenReturn(true); + when(webSocketSession.getHandshakeInfo()).thenReturn(handshakeInfo); + when(handshakeInfo.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 8080)); + + // When + sessionManager.register(sessionId, webSocketSession); + + // Wait a moment to ensure some time passes + Thread.sleep(100); + + // Then + List> activeSessions = sessionManager.getActiveAgentSessionDurations(); + assertEquals(1, activeSessions.size()); + + Map activeSession = activeSessions.get(0); + assertEquals(sessionId, activeSession.get("sessionId")); + assertEquals("agent", activeSession.get("sessionType")); + assertEquals(true, activeSession.get("active")); + assertNotNull(activeSession.get("startTime")); + assertNotNull(activeSession.get("durationMinutes")); + assertTrue((Long) activeSession.get("durationMinutes") >= 0); + } + + @Test + void testMultipleSessionsHandling() { + // Given + String sessionId1 = "session-1"; + String sessionId2 = "session-2"; + WebSocketSession session1 = mock(WebSocketSession.class); + WebSocketSession session2 = mock(WebSocketSession.class); + + when(session1.getId()).thenReturn(sessionId1); + when(session1.isOpen()).thenReturn(true); + when(session1.getHandshakeInfo()).thenReturn(handshakeInfo); + when(session2.getId()).thenReturn(sessionId2); + when(session2.isOpen()).thenReturn(true); + when(session2.getHandshakeInfo()).thenReturn(handshakeInfo); + when(handshakeInfo.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 8080)); + + // When + sessionManager.register(sessionId1, session1); + sessionManager.register(sessionId2, session2); + + // Then + assertEquals(2, sessionManager.getActiveSessions().size()); + assertEquals(2, sessionManager.getActiveAgentSessionDurations().size()); + + // When - unregister one session + sessionManager.unregister(sessionId1); + + // Then + assertEquals(1, sessionManager.getActiveSessions().size()); + assertEquals(1, sessionManager.getActiveAgentSessionDurations().size()); + assertEquals(1, sessionManager.getAgentSessionDurations().size()); + } + + @Test + void testUnregisterNonExistentSession() { + // Given + String nonExistentSessionId = "non-existent"; + + // When + sessionManager.unregister(nonExistentSessionId); + + // Then - should not throw exception and should not affect other data + assertEquals(0, sessionManager.getActiveSessions().size()); + assertEquals(0, sessionManager.getAgentSessionDurations().size()); + assertEquals(0, sessionManager.getActiveAgentSessionDurations().size()); + } +} \ No newline at end of file diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java index 5fe9d803..86a7ff21 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java @@ -7,9 +7,14 @@ import io.sentrius.sso.core.repository.SessionLogRepository; import io.sentrius.sso.core.repository.TerminalLogRepository; import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -21,6 +26,7 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +@Slf4j @Service public class SessionService { @@ -30,6 +36,11 @@ public class SessionService { @Autowired private TerminalLogRepository terminalLogRepository; + @Value("${agentproxy.externalUrl:}") + private String agentProxyExternalUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + private final Map activeSessions = new ConcurrentHashMap<>(); private final Map activeTerminals = new ConcurrentHashMap<>(); @@ -133,6 +144,10 @@ public List> getSessionDurationData(String username) { public Map getGraphData(String username) { List> sessionDurations = getSessionDurationData(username); + + // Add agent session durations + List> agentSessionDurations = getAgentSessionDurations(); + sessionDurations.addAll(agentSessionDurations); Map graphData = new HashMap<>(); graphData.put("0-5 min", 0); @@ -157,4 +172,52 @@ public Map getGraphData(String username) { return graphData; } + /** + * Fetch agent session duration data from agent proxy service + * @return List of agent session duration data + */ + private List> getAgentSessionDurations() { + List> agentSessions = new ArrayList<>(); + + if (agentProxyExternalUrl == null || agentProxyExternalUrl.trim().isEmpty()) { + log.warn("Agent proxy URL not configured, skipping agent session data"); + return agentSessions; + } + + try { + // Fetch completed agent sessions + String completedUrl = agentProxyExternalUrl + "/api/v1/sessions/agent/durations"; + var completedResponse = restTemplate.exchange( + completedUrl, + HttpMethod.GET, + null, + new ParameterizedTypeReference>>() {} + ); + + if (completedResponse.getBody() != null) { + agentSessions.addAll(completedResponse.getBody()); + } + + // Fetch active agent sessions + String activeUrl = agentProxyExternalUrl + "/api/v1/sessions/agent/active-durations"; + var activeResponse = restTemplate.exchange( + activeUrl, + HttpMethod.GET, + null, + new ParameterizedTypeReference>>() {} + ); + + if (activeResponse.getBody() != null) { + agentSessions.addAll(activeResponse.getBody()); + } + + log.info("Fetched {} agent session duration records", agentSessions.size()); + + } catch (Exception e) { + log.warn("Failed to fetch agent session data from {}: {}", agentProxyExternalUrl, e.getMessage()); + } + + return agentSessions; + } + } \ No newline at end of file diff --git a/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java b/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java new file mode 100644 index 00000000..d6adfef3 --- /dev/null +++ b/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java @@ -0,0 +1,214 @@ +package io.sentrius.sso.core.services; + +import io.sentrius.sso.core.model.sessions.SessionLog; +import io.sentrius.sso.core.repository.SessionLogRepository; +import io.sentrius.sso.core.repository.TerminalLogRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.RestTemplate; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SessionServiceTest { + + @Mock + private SessionLogRepository sessionLogRepository; + + @Mock + private TerminalLogRepository terminalLogRepository; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private SessionService sessionService; + + @BeforeEach + void setUp() { + // Set the agentProxyExternalUrl using ReflectionTestUtils + ReflectionTestUtils.setField(sessionService, "agentProxyExternalUrl", "http://test-agent-proxy"); + ReflectionTestUtils.setField(sessionService, "restTemplate", restTemplate); + } + + @Test + void testGetGraphDataWithoutAgentSessions() { + // Given + String username = "testuser"; + + // Mock terminal session data + when(sessionLogRepository.findByUsername(username)).thenReturn(Arrays.asList( + createSessionLog(1L, username), + createSessionLog(2L, username) + )); + + when(terminalLogRepository.findMinAndMaxLogTmBySessionLogId(1L)) + .thenReturn(Arrays.asList(new Object[]{ + Timestamp.valueOf(LocalDateTime.now().minusMinutes(10)), + Timestamp.valueOf(LocalDateTime.now().minusMinutes(5)) + })); + + when(terminalLogRepository.findMinAndMaxLogTmBySessionLogId(2L)) + .thenReturn(Arrays.asList(new Object[]{ + Timestamp.valueOf(LocalDateTime.now().minusMinutes(20)), + Timestamp.valueOf(LocalDateTime.now()) + })); + + // Mock agent service calls to return empty lists + when(restTemplate.exchange( + eq("http://test-agent-proxy/api/v1/sessions/agent/durations"), + eq(HttpMethod.GET), + isNull(), + any(ParameterizedTypeReference.class) + )).thenReturn(ResponseEntity.ok(new ArrayList<>())); + + when(restTemplate.exchange( + eq("http://test-agent-proxy/api/v1/sessions/agent/active-durations"), + eq(HttpMethod.GET), + isNull(), + any(ParameterizedTypeReference.class) + )).thenReturn(ResponseEntity.ok(new ArrayList<>())); + + // When + Map result = sessionService.getGraphData(username); + + // Then + assertNotNull(result); + assertEquals(4, result.size()); + assertTrue(result.containsKey("0-5 min")); + assertTrue(result.containsKey("5-15 min")); + assertTrue(result.containsKey("15-30 min")); + assertTrue(result.containsKey("30+ min")); + + // Should have 1 session in 0-5 min range and 1 in 15-30 min range + assertEquals(1, result.get("0-5 min")); + assertEquals(0, result.get("5-15 min")); + assertEquals(1, result.get("15-30 min")); + assertEquals(0, result.get("30+ min")); + } + + @Test + void testGetGraphDataWithAgentSessions() { + // Given + String username = "testuser"; + + // Mock terminal session data (empty for simplicity) + when(sessionLogRepository.findByUsername(username)).thenReturn(new ArrayList<>()); + + // Mock agent session data + List> completedAgentSessions = Arrays.asList( + createAgentSessionData("agent1", 3L), // 0-5 min + createAgentSessionData("agent2", 8L) // 5-15 min + ); + + List> activeAgentSessions = Arrays.asList( + createAgentSessionData("agent3", 25L) // 15-30 min + ); + + when(restTemplate.exchange( + eq("http://test-agent-proxy/api/v1/sessions/agent/durations"), + eq(HttpMethod.GET), + isNull(), + any(ParameterizedTypeReference.class) + )).thenReturn(ResponseEntity.ok(completedAgentSessions)); + + when(restTemplate.exchange( + eq("http://test-agent-proxy/api/v1/sessions/agent/active-durations"), + eq(HttpMethod.GET), + isNull(), + any(ParameterizedTypeReference.class) + )).thenReturn(ResponseEntity.ok(activeAgentSessions)); + + // When + Map result = sessionService.getGraphData(username); + + // Then + assertNotNull(result); + assertEquals(1, result.get("0-5 min")); // agent1 + assertEquals(1, result.get("5-15 min")); // agent2 + assertEquals(1, result.get("15-30 min")); // agent3 + assertEquals(0, result.get("30+ min")); + } + + @Test + void testGetGraphDataWithAgentProxyError() { + // Given + String username = "testuser"; + + // Mock terminal session data (empty for simplicity) + when(sessionLogRepository.findByUsername(username)).thenReturn(new ArrayList<>()); + + // Mock agent service calls to throw exception + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + isNull(), + any(ParameterizedTypeReference.class) + )).thenThrow(new RuntimeException("Connection failed")); + + // When + Map result = sessionService.getGraphData(username); + + // Then + assertNotNull(result); + // Should still return valid graph data even if agent service is unavailable + assertEquals(4, result.size()); + assertEquals(0, result.get("0-5 min")); + assertEquals(0, result.get("5-15 min")); + assertEquals(0, result.get("15-30 min")); + assertEquals(0, result.get("30+ min")); + } + + @Test + void testGetGraphDataWithEmptyAgentProxyUrl() { + // Given + String username = "testuser"; + ReflectionTestUtils.setField(sessionService, "agentProxyExternalUrl", ""); + + // Mock terminal session data (empty for simplicity) + when(sessionLogRepository.findByUsername(username)).thenReturn(new ArrayList<>()); + + // When + Map result = sessionService.getGraphData(username); + + // Then + assertNotNull(result); + assertEquals(4, result.size()); + // Should not attempt to call agent proxy + verify(restTemplate, never()).exchange(anyString(), any(), any(), any(ParameterizedTypeReference.class)); + } + + private SessionLog createSessionLog(Long id, String username) { + SessionLog session = new SessionLog(); + session.setId(id); + session.setUsername(username); + session.setSessionTm(new Timestamp(System.currentTimeMillis())); + return session; + } + + private Map createAgentSessionData(String sessionId, Long durationMinutes) { + Map sessionData = new HashMap<>(); + sessionData.put("sessionId", sessionId); + sessionData.put("durationMinutes", durationMinutes); + sessionData.put("sessionType", "agent"); + return sessionData; + } +} \ No newline at end of file From 8859e84ecfc4ef99e036b2ca662d9352a5b705fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 12:10:58 +0000 Subject: [PATCH 3/4] Fix compilation error in SessionServiceTest due to type mismatch Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> --- .../sso/service/ActiveWebSocketSessionManagerTest.java | 4 ---- .../io/sentrius/sso/core/services/SessionServiceTest.java | 5 +++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/agent-proxy/src/test/java/io/sentrius/sso/service/ActiveWebSocketSessionManagerTest.java b/agent-proxy/src/test/java/io/sentrius/sso/service/ActiveWebSocketSessionManagerTest.java index 7394a942..cdaaeba3 100644 --- a/agent-proxy/src/test/java/io/sentrius/sso/service/ActiveWebSocketSessionManagerTest.java +++ b/agent-proxy/src/test/java/io/sentrius/sso/service/ActiveWebSocketSessionManagerTest.java @@ -72,10 +72,6 @@ void testRegisterAndUnregisterSession() { void testGetActiveAgentSessionDurations() throws InterruptedException { // Given String sessionId = "active-session-1"; - when(webSocketSession.getId()).thenReturn(sessionId); - when(webSocketSession.isOpen()).thenReturn(true); - when(webSocketSession.getHandshakeInfo()).thenReturn(handshakeInfo); - when(handshakeInfo.getRemoteAddress()).thenReturn(new InetSocketAddress("127.0.0.1", 8080)); // When sessionManager.register(sessionId, webSocketSession); diff --git a/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java b/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java index d6adfef3..0d8d3580 100644 --- a/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java +++ b/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java @@ -19,6 +19,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -61,13 +62,13 @@ void testGetGraphDataWithoutAgentSessions() { )); when(terminalLogRepository.findMinAndMaxLogTmBySessionLogId(1L)) - .thenReturn(Arrays.asList(new Object[]{ + .thenReturn(Collections.singletonList(new Object[]{ Timestamp.valueOf(LocalDateTime.now().minusMinutes(10)), Timestamp.valueOf(LocalDateTime.now().minusMinutes(5)) })); when(terminalLogRepository.findMinAndMaxLogTmBySessionLogId(2L)) - .thenReturn(Arrays.asList(new Object[]{ + .thenReturn(Collections.singletonList(new Object[]{ Timestamp.valueOf(LocalDateTime.now().minusMinutes(20)), Timestamp.valueOf(LocalDateTime.now()) })); From 89ccaad4ef41be9db5291a64ac75f73d04496cea Mon Sep 17 00:00:00 2001 From: Marc Parisi Date: Sat, 26 Jul 2025 11:55:39 -0400 Subject: [PATCH 4/4] fix tests --- .local.env | 4 +- .local.env.bak | 4 +- .../sentrius/sso/config/SecurityConfig.java | 1 + .../ActiveWebSocketSessionManager.java | 5 +- .../controllers/api/UserApiController.java | 124 +++++++++++++++++- .../sso/core/services/SessionService.java | 64 ++------- .../sso/core/services/SessionServiceTest.java | 44 +------ 7 files changed, 143 insertions(+), 103 deletions(-) diff --git a/.local.env b/.local.env index c83b4231..a94ea226 100644 --- a/.local.env +++ b/.local.env @@ -1,8 +1,8 @@ -SENTRIUS_VERSION=1.1.325 +SENTRIUS_VERSION=1.1.334 SENTRIUS_SSH_VERSION=1.1.41 SENTRIUS_KEYCLOAK_VERSION=1.1.53 SENTRIUS_AGENT_VERSION=1.1.42 SENTRIUS_AI_AGENT_VERSION=1.1.263 LLMPROXY_VERSION=1.0.78 LAUNCHER_VERSION=1.0.82 -AGENTPROXY_VERSION=1.0.75 \ No newline at end of file +AGENTPROXY_VERSION=1.0.85 \ No newline at end of file diff --git a/.local.env.bak b/.local.env.bak index c83b4231..a94ea226 100644 --- a/.local.env.bak +++ b/.local.env.bak @@ -1,8 +1,8 @@ -SENTRIUS_VERSION=1.1.325 +SENTRIUS_VERSION=1.1.334 SENTRIUS_SSH_VERSION=1.1.41 SENTRIUS_KEYCLOAK_VERSION=1.1.53 SENTRIUS_AGENT_VERSION=1.1.42 SENTRIUS_AI_AGENT_VERSION=1.1.263 LLMPROXY_VERSION=1.0.78 LAUNCHER_VERSION=1.0.82 -AGENTPROXY_VERSION=1.0.75 \ No newline at end of file +AGENTPROXY_VERSION=1.0.85 \ No newline at end of file diff --git a/agent-proxy/src/main/java/io/sentrius/sso/config/SecurityConfig.java b/agent-proxy/src/main/java/io/sentrius/sso/config/SecurityConfig.java index 17cf55df..3b3a0ac7 100644 --- a/agent-proxy/src/main/java/io/sentrius/sso/config/SecurityConfig.java +++ b/agent-proxy/src/main/java/io/sentrius/sso/config/SecurityConfig.java @@ -65,6 +65,7 @@ private ReactiveJwtAuthenticationConverter grantedAuthoritiesExtractor() { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); + log.info("Configuring CORS for agent API URL: {}", agentApiUrl); config.setAllowedOrigins(List.of(agentApiUrl)); config.setAllowedMethods(List.of("GET", "POST", "OPTIONS")); config.setAllowedHeaders(List.of("*")); diff --git a/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java b/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java index 162cbc02..6736bbe2 100644 --- a/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java +++ b/agent-proxy/src/main/java/io/sentrius/sso/service/ActiveWebSocketSessionManager.java @@ -11,9 +11,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import io.sentrius.sso.core.dto.TerminalLogDTO; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.reactive.socket.WebSocketSession; +@Slf4j @Component public class ActiveWebSocketSessionManager { private final Map sessions = new ConcurrentHashMap<>(); @@ -72,6 +74,7 @@ public List getActiveSessions() { */ public List> getAgentSessionDurations() { synchronized (completedAgentSessions) { + log.info("Returning {} completed agent sessions", completedAgentSessions.size()); return new ArrayList<>(completedAgentSessions); } } @@ -103,7 +106,7 @@ public List> getActiveAgentSessionDurations() { activeDurations.add(activeSession); } } - + log.info("Returning {} active agent session durations", activeDurations.size()); return activeDurations; } } diff --git a/api/src/main/java/io/sentrius/sso/controllers/api/UserApiController.java b/api/src/main/java/io/sentrius/sso/controllers/api/UserApiController.java index c8b13704..45e17f2c 100644 --- a/api/src/main/java/io/sentrius/sso/controllers/api/UserApiController.java +++ b/api/src/main/java/io/sentrius/sso/controllers/api/UserApiController.java @@ -2,6 +2,7 @@ import java.lang.reflect.Field; import java.security.GeneralSecurityException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -14,6 +15,7 @@ import io.sentrius.sso.core.annotations.LimitAccess; import io.sentrius.sso.core.config.SystemOptions; import io.sentrius.sso.core.controllers.BaseController; +import io.sentrius.sso.core.exceptions.ZtatException; import io.sentrius.sso.core.model.security.UserType; import io.sentrius.sso.core.model.security.enums.UserAccessEnum; import io.sentrius.sso.core.model.users.User; @@ -27,6 +29,7 @@ import io.sentrius.sso.core.services.UserCustomizationService; import io.sentrius.sso.core.services.UserService; import io.sentrius.sso.core.services.agents.AgentService; +import io.sentrius.sso.core.services.agents.ZeroTrustClientService; import io.sentrius.sso.core.services.security.CryptoService; import io.sentrius.sso.core.services.security.ZeroTrustAccessTokenService; import io.sentrius.sso.core.services.security.ZeroTrustRequestService; @@ -35,6 +38,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -60,6 +66,11 @@ public class UserApiController extends BaseController { final ZeroTrustRequestService ztatRequestService; final ZeroTrustAccessTokenService ztatService; final AgentService agentService; + final ZeroTrustClientService zeroTrustClientService; + + + @Value("${agentproxy.externalUrl:}") + private String agentProxyExternalUrl; static Map fields = new HashMap<>(); static { @@ -78,7 +89,8 @@ protected UserApiController( UserCustomizationService userThemeService, SessionService sessionService, ZeroTrustRequestService ztatRequestService, - ZeroTrustAccessTokenService ztatService, AgentService agentService + ZeroTrustAccessTokenService ztatService, AgentService agentService, + ZeroTrustClientService zeroTrustClientService ) { super(userService, systemOptions, errorOutputService); this.hostGroupService = hostGroupService; @@ -89,6 +101,7 @@ protected UserApiController( this.ztatRequestService = ztatRequestService; this.ztatService = ztatService; this.agentService = agentService; + this.zeroTrustClientService = zeroTrustClientService; } @GetMapping("list") @@ -303,10 +316,117 @@ public String deleteType(@RequestParam("id") String dtoId) throws GeneralSecurit public ResponseEntity> getGraphData(HttpServletRequest request, HttpServletResponse response) { var username = userService.getOperatingUser(request,response, null).getUsername(); - var ret= sessionService.getGraphData(username); + var ret= getGraphData(username); return ResponseEntity.ok(ret); } + + public Map getGraphData(String username) { + List> sessionDurations = sessionService.getGraphList(username); + + // Add agent session durations + List> agentSessionDurations = getAgentSessionDurations(); + log.info("Fetched {} agent session durations", agentSessionDurations.size()); + log.info("Fetched {} agent session durations", agentSessionDurations.size()); + sessionDurations.addAll(agentSessionDurations); + + Map graphData = new HashMap<>(); + graphData.put("0-5 min", 0); + graphData.put("5-15 min", 0); + graphData.put("15-30 min", 0); + graphData.put("30+ min", 0); + + for (Map session : sessionDurations) { + long durationMinutes = Long.valueOf ( session.get("durationMinutes").toString() ); + + if (durationMinutes <= 5) { + graphData.put("0-5 min", graphData.get("0-5 min") + 1); + } else if (durationMinutes <= 15) { + graphData.put("5-15 min", graphData.get("5-15 min") + 1); + } else if (durationMinutes <= 30) { + graphData.put("15-30 min", graphData.get("15-30 min") + 1); + } else { + graphData.put("30+ min", graphData.get("30+ min") + 1); + } + } + + return graphData; + } + + /** + * Fetch agent session duration data from agent proxy service + * @return List of agent session duration data + */ + private List> getAgentSessionDurations() { + List> agentSessions = new ArrayList<>(); + + if (agentProxyExternalUrl == null || agentProxyExternalUrl.trim().isEmpty()) { + log.warn("Agent proxy URL not configured, skipping agent session data"); + return agentSessions; + } + + try { + + var resp = zeroTrustClientService.callAuthenticatedGetOnApi(agentProxyExternalUrl,"/api/v1/sessions/agent" + + "/durations" + , null); + log.info("Fetched active agent session duration data: {}", resp); + if (null != resp){ + var completedNode = JsonUtil.MAPPER.readTree(resp); + Map completedMap = new HashMap<>(); + for(var node : completedNode) { + node.fields().forEachRemaining( + entry -> { + completedMap.put(entry.getKey(), entry.getValue()); + log.info( + "Processing agent session duration entry: {} = {}", entry.getKey(), entry.getValue()); + } + + ); + } + if (completedMap.size() > 0) { + log.info("Adding completed agent session duration data: {}", completedMap); + agentSessions.add(completedMap); + } + + } + + + resp = zeroTrustClientService.callAuthenticatedGetOnApi(agentProxyExternalUrl,"/api/v1/sessions/agent/active-durations" + , null); + + log.info("Fetched active agent session duration data: {}", resp); + if (null != resp){ + var completedNode = JsonUtil.MAPPER.readTree(resp); + Map completedMap = new HashMap<>(); + for(var node : completedNode) { + node.fields().forEachRemaining( + entry -> { + completedMap.put(entry.getKey(), entry.getValue()); + log.info( + "Processing agent session duration entry: {} = {}", entry.getKey(), entry.getValue()); + } + + ); + } + if (completedMap.size() > 0) { + log.info("Adding completed agent session duration data: {}", completedMap); + agentSessions.add(completedMap); + } + + } + + log.info("Fetched {} agent session duration records", agentSessions.size()); + + } catch (Exception e) { + log.warn("Failed to fetch agent session data from {}: {}", agentProxyExternalUrl, e.getMessage()); + } catch (ZtatException e) { + log.warn("Failed to fetch agent session data from {}: {}", agentProxyExternalUrl, e.getMessage()); + } + + return agentSessions; + } + } diff --git a/dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java b/dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java index 86a7ff21..e338fe36 100644 --- a/dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java +++ b/dataplane/src/main/java/io/sentrius/sso/core/services/SessionService.java @@ -10,11 +10,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.client.RestTemplate; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -36,11 +33,10 @@ public class SessionService { @Autowired private TerminalLogRepository terminalLogRepository; + @Value("${agentproxy.externalUrl:}") private String agentProxyExternalUrl; - private final RestTemplate restTemplate = new RestTemplate(); - private final Map activeSessions = new ConcurrentHashMap<>(); private final Map activeTerminals = new ConcurrentHashMap<>(); @@ -144,11 +140,8 @@ public List> getSessionDurationData(String username) { public Map getGraphData(String username) { List> sessionDurations = getSessionDurationData(username); - - // Add agent session durations - List> agentSessionDurations = getAgentSessionDurations(); - sessionDurations.addAll(agentSessionDurations); + // Add agent session durations Map graphData = new HashMap<>(); graphData.put("0-5 min", 0); graphData.put("5-15 min", 0); @@ -172,52 +165,13 @@ public Map getGraphData(String username) { return graphData; } - /** - * Fetch agent session duration data from agent proxy service - * @return List of agent session duration data - */ - private List> getAgentSessionDurations() { - List> agentSessions = new ArrayList<>(); - - if (agentProxyExternalUrl == null || agentProxyExternalUrl.trim().isEmpty()) { - log.warn("Agent proxy URL not configured, skipping agent session data"); - return agentSessions; - } - - try { - // Fetch completed agent sessions - String completedUrl = agentProxyExternalUrl + "/api/v1/sessions/agent/durations"; - var completedResponse = restTemplate.exchange( - completedUrl, - HttpMethod.GET, - null, - new ParameterizedTypeReference>>() {} - ); - - if (completedResponse.getBody() != null) { - agentSessions.addAll(completedResponse.getBody()); - } - - // Fetch active agent sessions - String activeUrl = agentProxyExternalUrl + "/api/v1/sessions/agent/active-durations"; - var activeResponse = restTemplate.exchange( - activeUrl, - HttpMethod.GET, - null, - new ParameterizedTypeReference>>() {} - ); - - if (activeResponse.getBody() != null) { - agentSessions.addAll(activeResponse.getBody()); - } - - log.info("Fetched {} agent session duration records", agentSessions.size()); - - } catch (Exception e) { - log.warn("Failed to fetch agent session data from {}: {}", agentProxyExternalUrl, e.getMessage()); - } - - return agentSessions; + public List> getGraphList(String username) { + List> sessionDurations = getSessionDurationData(username); + + return sessionDurations; } + + + } \ No newline at end of file diff --git a/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java b/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java index 0d8d3580..3917f515 100644 --- a/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java +++ b/dataplane/src/test/java/io/sentrius/sso/core/services/SessionServiceTest.java @@ -37,8 +37,6 @@ class SessionServiceTest { @Mock private TerminalLogRepository terminalLogRepository; - @Mock - private RestTemplate restTemplate; @InjectMocks private SessionService sessionService; @@ -47,7 +45,6 @@ class SessionServiceTest { void setUp() { // Set the agentProxyExternalUrl using ReflectionTestUtils ReflectionTestUtils.setField(sessionService, "agentProxyExternalUrl", "http://test-agent-proxy"); - ReflectionTestUtils.setField(sessionService, "restTemplate", restTemplate); } @Test @@ -74,20 +71,6 @@ void testGetGraphDataWithoutAgentSessions() { })); // Mock agent service calls to return empty lists - when(restTemplate.exchange( - eq("http://test-agent-proxy/api/v1/sessions/agent/durations"), - eq(HttpMethod.GET), - isNull(), - any(ParameterizedTypeReference.class) - )).thenReturn(ResponseEntity.ok(new ArrayList<>())); - - when(restTemplate.exchange( - eq("http://test-agent-proxy/api/v1/sessions/agent/active-durations"), - eq(HttpMethod.GET), - isNull(), - any(ParameterizedTypeReference.class) - )).thenReturn(ResponseEntity.ok(new ArrayList<>())); - // When Map result = sessionService.getGraphData(username); @@ -124,28 +107,15 @@ void testGetGraphDataWithAgentSessions() { createAgentSessionData("agent3", 25L) // 15-30 min ); - when(restTemplate.exchange( - eq("http://test-agent-proxy/api/v1/sessions/agent/durations"), - eq(HttpMethod.GET), - isNull(), - any(ParameterizedTypeReference.class) - )).thenReturn(ResponseEntity.ok(completedAgentSessions)); - - when(restTemplate.exchange( - eq("http://test-agent-proxy/api/v1/sessions/agent/active-durations"), - eq(HttpMethod.GET), - isNull(), - any(ParameterizedTypeReference.class) - )).thenReturn(ResponseEntity.ok(activeAgentSessions)); // When Map result = sessionService.getGraphData(username); // Then assertNotNull(result); - assertEquals(1, result.get("0-5 min")); // agent1 - assertEquals(1, result.get("5-15 min")); // agent2 - assertEquals(1, result.get("15-30 min")); // agent3 + assertEquals(0, result.get("0-5 min")); // agent1 + assertEquals(0, result.get("5-15 min")); // agent2 + assertEquals(0, result.get("15-30 min")); // agent3 assertEquals(0, result.get("30+ min")); } @@ -157,13 +127,6 @@ void testGetGraphDataWithAgentProxyError() { // Mock terminal session data (empty for simplicity) when(sessionLogRepository.findByUsername(username)).thenReturn(new ArrayList<>()); - // Mock agent service calls to throw exception - when(restTemplate.exchange( - anyString(), - eq(HttpMethod.GET), - isNull(), - any(ParameterizedTypeReference.class) - )).thenThrow(new RuntimeException("Connection failed")); // When Map result = sessionService.getGraphData(username); @@ -194,7 +157,6 @@ void testGetGraphDataWithEmptyAgentProxyUrl() { assertNotNull(result); assertEquals(4, result.size()); // Should not attempt to call agent proxy - verify(restTemplate, never()).exchange(anyString(), any(), any(), any(ParameterizedTypeReference.class)); } private SessionLog createSessionLog(Long id, String username) {