diff --git a/api/src/main/java/org/openmrs/module/auditlogweb/api/AuditService.java b/api/src/main/java/org/openmrs/module/auditlogweb/api/AuditService.java index 30ea5a8e..59444ca8 100644 --- a/api/src/main/java/org/openmrs/module/auditlogweb/api/AuditService.java +++ b/api/src/main/java/org/openmrs/module/auditlogweb/api/AuditService.java @@ -334,10 +334,20 @@ void logSecurityEvent(AuditSecurityEventType eventType, String username, String * Retrieves the most recent N security events from the same session (for related activity). * * @param sessionId the session ID to filter by - * @param limit the maximum number of events to return + * @param page defines the particular page in a paginated view + * @param size defines how many logs want in one go * @return a list of {@link AuditSecurityEvent} ordered by eventTime descending */ @Authorized(AuditLogConstants.VIEW_SECURITY_AUDIT_LOGS) - List getRelatedSecurityEvents(String sessionId, int limit); + List getRelatedSecurityEvents(String sessionId, int page, int size); + + /** + * Counts the related security audit events with session ID filter. + * + * @param sessionId the session ID to filter by + * @return the number of logs found + */ + @Authorized(AuditLogConstants.VIEW_SECURITY_AUDIT_LOGS) + long countRelatedSecurityEvents(String sessionId); } diff --git a/api/src/main/java/org/openmrs/module/auditlogweb/api/dao/AuditDao.java b/api/src/main/java/org/openmrs/module/auditlogweb/api/dao/AuditDao.java index 993d10e8..4e23887c 100644 --- a/api/src/main/java/org/openmrs/module/auditlogweb/api/dao/AuditDao.java +++ b/api/src/main/java/org/openmrs/module/auditlogweb/api/dao/AuditDao.java @@ -714,14 +714,30 @@ public AuditSecurityEvent getSecurityEventById(Integer eventId) { * Retrieves the most recent N security events from the same session (for related activity). * * @param sessionId the session ID to filter by - * @param limit the maximum number of events to return + * @param page defines the particular page in a paginated view + * @param size defines how many logs want in one go * @return a list of {@link AuditSecurityEvent} ordered by eventTime descending */ - public List getRelatedSecurityEvents(String sessionId, int limit) { + public List getRelatedSecurityEvents(String sessionId, int page, int size) { Query query = sessionFactory.getCurrentSession().createQuery( "from AuditSecurityEvent e where e.sessionId = :sessionId order by e.eventTime desc", AuditSecurityEvent.class); query.setParameter("sessionId", sessionId); - query.setMaxResults(limit); + query.setFirstResult(page * size); + query.setMaxResults(size); return query.getResultList(); } + + /** + * Returns the count for all related security events for the given session ID + * + * @param sessionId the session ID to filter by + * @return the count of logs found + */ + public long countRelatedSecurityEvents(String sessionId) { + Query query = sessionFactory.getCurrentSession() + .createQuery("select count(e) from AuditSecurityEvent e where e.sessionId = :sessionId", Long.class); + query.setParameter("sessionId", sessionId); + Long result = query.uniqueResult(); + return result != null ? result : 0L; + } } diff --git a/api/src/main/java/org/openmrs/module/auditlogweb/api/dto/SecurityAuditLogDTO.java b/api/src/main/java/org/openmrs/module/auditlogweb/api/dto/SecurityAuditLogDTO.java new file mode 100644 index 00000000..4750e310 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/auditlogweb/api/dto/SecurityAuditLogDTO.java @@ -0,0 +1,40 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.auditlogweb.api.dto; + +import lombok.Builder; +import lombok.Data; +import org.openmrs.module.auditlogweb.api.utils.AuditSecurityEventType; + +import java.util.Date; + +@Data +@Builder +public class SecurityAuditLogDTO { + + private Integer id; + + private AuditSecurityEventType eventType; + + private String username; + + private String userUuid; + + private Date eventTime; + + private String ipAddress; + + private String userAgent; + + private String sessionId; + + private String details; + +} diff --git a/api/src/main/java/org/openmrs/module/auditlogweb/api/dto/SecurityLogResponseDTO.java b/api/src/main/java/org/openmrs/module/auditlogweb/api/dto/SecurityLogResponseDTO.java new file mode 100644 index 00000000..27a3a2c6 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/auditlogweb/api/dto/SecurityLogResponseDTO.java @@ -0,0 +1,31 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.auditlogweb.api.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class SecurityLogResponseDTO { + + private long totalLogs; + + private int currentLogs; + + private int totalPages; + + private int currentPage; + + private List securityAuditLogs; + +} diff --git a/api/src/main/java/org/openmrs/module/auditlogweb/api/impl/AuditServiceImpl.java b/api/src/main/java/org/openmrs/module/auditlogweb/api/impl/AuditServiceImpl.java index 599d7a9e..228d77da 100644 --- a/api/src/main/java/org/openmrs/module/auditlogweb/api/impl/AuditServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/auditlogweb/api/impl/AuditServiceImpl.java @@ -387,8 +387,13 @@ public AuditSecurityEvent getSecurityEventById(Integer eventId) { } @Override - public List getRelatedSecurityEvents(String sessionId, int limit) { - return auditDao.getRelatedSecurityEvents(sessionId, limit); + public List getRelatedSecurityEvents(String sessionId, int page, int size) { + return auditDao.getRelatedSecurityEvents(sessionId, page, size); + } + + @Override + public long countRelatedSecurityEvents(String sessionId) { + return auditDao.countRelatedSecurityEvents(sessionId); } private Object fetchPreviousRevision(AuditEntity entity, Object currentEntity) { diff --git a/api/src/test/java/org/openmrs/module/auditlogweb/api/dao/AuditDaoTest.java b/api/src/test/java/org/openmrs/module/auditlogweb/api/dao/AuditDaoTest.java index 7967f208..1692280d 100644 --- a/api/src/test/java/org/openmrs/module/auditlogweb/api/dao/AuditDaoTest.java +++ b/api/src/test/java/org/openmrs/module/auditlogweb/api/dao/AuditDaoTest.java @@ -580,30 +580,45 @@ void shouldReturnRelatedSecurityEvents_WhenSessionIdProvided() { when(session.createQuery(anyString(), eq(AuditSecurityEvent.class))).thenReturn(securityEventQuery); when(securityEventQuery.setParameter("sessionId", sessionId)).thenReturn(securityEventQuery); - when(securityEventQuery.setMaxResults(5)).thenReturn(securityEventQuery); + when(securityEventQuery.setFirstResult(30)).thenReturn(securityEventQuery); + when(securityEventQuery.setMaxResults(2)).thenReturn(securityEventQuery); when(securityEventQuery.getResultList()).thenReturn(Arrays.asList(e1, e2)); - List result = auditDao.getRelatedSecurityEvents(sessionId, 5); + List result = auditDao.getRelatedSecurityEvents(sessionId, 15, 2); assertNotNull(result); assertThat(result, hasSize(2)); verify(securityEventQuery).setParameter("sessionId", sessionId); - verify(securityEventQuery).setMaxResults(5); + verify(securityEventQuery).setFirstResult(30); + verify(securityEventQuery).setMaxResults(2); } @Test void shouldReturnEmptyList_WhenNoRelatedSecurityEventsFound() { when(session.createQuery(anyString(), eq(AuditSecurityEvent.class))).thenReturn(securityEventQuery); when(securityEventQuery.setParameter(anyString(), anyString())).thenReturn(securityEventQuery); + when(securityEventQuery.setFirstResult(0)).thenReturn(securityEventQuery); when(securityEventQuery.setMaxResults(anyInt())).thenReturn(securityEventQuery); when(securityEventQuery.getResultList()).thenReturn(Collections.emptyList()); - List result = auditDao.getRelatedSecurityEvents("sess-ghost", 10); + List result = auditDao.getRelatedSecurityEvents("sess-ghost", 0, 10); assertNotNull(result); assertThat(result, empty()); } + @Test + void shouldReturnRelatedSecurityEventsCount() { + when(session.createQuery(anyString(), eq(Long.class))).thenReturn(countQuery); + when(countQuery.setParameter(eq("sessionId"), eq("session-123"))).thenReturn(countQuery); + when(countQuery.uniqueResult()).thenReturn(10L); + + long count = auditDao.countRelatedSecurityEvents("session-123"); + + assertThat(count, is(10L)); + verify(countQuery).setParameter("sessionId", "session-123"); + } + @Test void shouldBindUnknownEventType_WhenGivenSecurityEventIsInvalid() { when(session.createQuery(anyString(), eq(AuditSecurityEvent.class))).thenReturn(securityEventQuery); diff --git a/api/src/test/java/org/openmrs/module/auditlogweb/api/impl/AuditServiceImplTest.java b/api/src/test/java/org/openmrs/module/auditlogweb/api/impl/AuditServiceImplTest.java index fd6c8b5c..fad6b1c8 100644 --- a/api/src/test/java/org/openmrs/module/auditlogweb/api/impl/AuditServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/auditlogweb/api/impl/AuditServiceImplTest.java @@ -397,10 +397,10 @@ void shouldReturnRelatedSecurityEvents() { AuditSecurityEvent relatedSecurityEvent2 = AuditSecurityEvent.builder() .eventType(AuditSecurityEventType.LOGIN_SUCCESS).eventTime(new Date()).sessionId("session-123").build(); - when(auditDao.getRelatedSecurityEvents("session-123", 2)) + when(auditDao.getRelatedSecurityEvents("session-123", 0, 2)) .thenReturn(Arrays.asList(relatedSecurityEvent1, relatedSecurityEvent2)); - List result = auditService.getRelatedSecurityEvents("session-123", 2); + List result = auditService.getRelatedSecurityEvents("session-123", 0, 2); assertNotNull(result); assertEquals(2, result.size()); @@ -408,6 +408,14 @@ void shouldReturnRelatedSecurityEvents() { assertEquals(relatedSecurityEvent2, result.get(1)); } + @Test + void shouldCountRelatedSecurityEvents() { + when(auditDao.countRelatedSecurityEvents("session-123")).thenReturn(10L); + + long count = auditService.countRelatedSecurityEvents("session-123"); + assertEquals(10L, count); + } + public static class TestEntity { private Integer id; diff --git a/omod/src/main/java/org/openmrs/module/auditlogweb/rest/ReadAuditRestController.java b/omod/src/main/java/org/openmrs/module/auditlogweb/rest/ReadAuditRestController.java index 5b54ba98..8d14289f 100644 --- a/omod/src/main/java/org/openmrs/module/auditlogweb/rest/ReadAuditRestController.java +++ b/omod/src/main/java/org/openmrs/module/auditlogweb/rest/ReadAuditRestController.java @@ -81,7 +81,7 @@ public ReadAuditLogResponseDTO fetchReadAudits(@RequestParam(value = "logId", re } - @GetMapping("/releatedAudits") + @GetMapping("/relatedAudits") public ReadAuditLogResponseDTO fetchRelatedAudits(@RequestParam(value = "sessionId") String sessionId, @RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "15") int size) { diff --git a/omod/src/main/java/org/openmrs/module/auditlogweb/rest/SecurityAuditRestController.java b/omod/src/main/java/org/openmrs/module/auditlogweb/rest/SecurityAuditRestController.java new file mode 100644 index 00000000..76c4372a --- /dev/null +++ b/omod/src/main/java/org/openmrs/module/auditlogweb/rest/SecurityAuditRestController.java @@ -0,0 +1,135 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.auditlogweb.rest; + +import lombok.RequiredArgsConstructor; +import org.openmrs.module.auditlogweb.AuditSecurityEvent; +import org.openmrs.module.auditlogweb.api.AuditService; +import org.openmrs.module.auditlogweb.api.dto.SecurityAuditLogDTO; +import org.openmrs.module.auditlogweb.api.dto.SecurityLogResponseDTO; +import org.openmrs.module.auditlogweb.api.utils.AuditSecurityEventType; +import org.openmrs.module.auditlogweb.api.utils.UtilClass; +import org.openmrs.module.webservices.rest.web.RestConstants; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +@RestController +@RequestMapping("/rest/" + RestConstants.VERSION_1 + "/securityauditlogs") +@RequiredArgsConstructor +public class SecurityAuditRestController { + + private final AuditService auditService; + + @GetMapping + public SecurityLogResponseDTO fetchSecurityAudits(@RequestParam(value = "logId", required = false) Integer logId, + @RequestParam(value = "eventType", required = false) String eventType, + @RequestParam(value = "username", required = false) String username, + @RequestParam(value = "startDate", required = false) String startDate, + @RequestParam(value = "endDate", required = false) String endDate, + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "15") int size) { + + if (logId != null && logId <= 0) { + throw new IllegalArgumentException("Please provide a valid log ID"); + } + + if (eventType != null && !eventType.trim().isEmpty()) { + AuditSecurityEventType parsed = AuditSecurityEventType.fromName(eventType); + if (parsed == null || parsed == AuditSecurityEventType.UNKNOWN) { + throw new IllegalArgumentException("Invalid eventType " + eventType); + } + } + + if (logId != null) { + AuditSecurityEvent securityEvent = auditService.getSecurityEventById(logId); + if (securityEvent == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No log found for this logId"); + } + List securityAuditLogsDTO = mapToDTOs(Collections.singletonList(securityEvent)); + return SecurityLogResponseDTO.builder().totalLogs(1).currentLogs(1).securityAuditLogs(securityAuditLogsDTO) + .totalPages(1).currentPage(0).build(); + } + + if (page < 0) { + page = 0; + } + if (size <= 0) { + size = 15; + } + + Date start = UtilClass.parseDate(startDate, false); + Date end = UtilClass.parseDate(endDate, true); + + List securityEvents = auditService.getSecurityEvents(eventType, username, start, end, page, + size); + long totalCount = auditService.countSecurityEvents(eventType, username, start, end); + int totalPages = UtilClass.computeTotalPages(totalCount, size); + + List securityAuditLogsDTO = mapToDTOs(securityEvents); + + return SecurityLogResponseDTO.builder().totalLogs(totalCount).currentLogs(securityAuditLogsDTO.size()) + .totalPages(totalPages).currentPage(page).securityAuditLogs(securityAuditLogsDTO).build(); + } + + @GetMapping("/relatedAudits") + public SecurityLogResponseDTO fetchRelatedAudits(@RequestParam(value = "sessionId") String sessionId, + @RequestParam(value = "page", defaultValue = "0") int page, + @RequestParam(value = "size", defaultValue = "15") int size) { + + if (sessionId == null || sessionId.isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid session id"); + } + + if (page < 0) { + page = 0; + } + if (size <= 0) { + size = 15; + } + + List allRelated = auditService.getRelatedSecurityEvents(sessionId, page, size); + long totalCount = auditService.countRelatedSecurityEvents(sessionId); + int totalPages = UtilClass.computeTotalPages(totalCount, size); + + List securityAuditLogsDTO = mapToDTOs(allRelated); + + return SecurityLogResponseDTO.builder().totalLogs(totalCount).currentLogs(securityAuditLogsDTO.size()) + .totalPages(totalPages).currentPage(page).securityAuditLogs(securityAuditLogsDTO).build(); + } + + private SecurityAuditLogDTO mapToDTO(AuditSecurityEvent event) { + if (event == null) { + return null; + } + return SecurityAuditLogDTO.builder().id(event.getId()).eventType(event.getEventType()).username(event.getUsername()) + .userUuid(event.getUserUuid()).eventTime(event.getEventTime()).ipAddress(event.getIpAddress()) + .userAgent(event.getUserAgent()).sessionId(event.getSessionId()).details(event.getDetails()).build(); + } + + private List mapToDTOs(List events) { + if (events == null) { + return Collections.emptyList(); + } + List dtos = new ArrayList<>(events.size()); + for (AuditSecurityEvent event : events) { + dtos.add(mapToDTO(event)); + } + return dtos; + } +} diff --git a/omod/src/main/java/org/openmrs/module/auditlogweb/rest/exceptions/RestExceptionHandler.java b/omod/src/main/java/org/openmrs/module/auditlogweb/rest/exceptions/RestExceptionHandler.java index be6c6356..6ca0f20c 100644 --- a/omod/src/main/java/org/openmrs/module/auditlogweb/rest/exceptions/RestExceptionHandler.java +++ b/omod/src/main/java/org/openmrs/module/auditlogweb/rest/exceptions/RestExceptionHandler.java @@ -10,6 +10,8 @@ package org.openmrs.module.auditlogweb.rest.exceptions; import org.hibernate.ObjectNotFoundException; +import org.openmrs.api.APIAuthenticationException; +import org.openmrs.api.context.Context; import org.openmrs.module.auditlogweb.api.exception.AuditLogUnavailableException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -84,6 +86,14 @@ public ResponseEntity> handleAuditLogUnavailable(AuditLogUna return buildResponseEntity("Audit Log Unavailable", ex.getMessage(), HttpStatus.SERVICE_UNAVAILABLE); } + @ExceptionHandler(APIAuthenticationException.class) + public ResponseEntity> handleAPIAuthException(APIAuthenticationException ex) { + if (Context.isAuthenticated()) { + return buildResponseEntity("Forbidden", ex.getMessage(), HttpStatus.FORBIDDEN); + } + return buildResponseEntity("Unauthorized", ex.getMessage(), HttpStatus.UNAUTHORIZED); + } + @ExceptionHandler(Exception.class) public ResponseEntity> handleGeneralError(Exception ex) { return buildResponseEntity("Internal Server Error", "An unexpected error occurred", diff --git a/omod/src/main/java/org/openmrs/module/auditlogweb/web/controller/SecurityAuditDetailController.java b/omod/src/main/java/org/openmrs/module/auditlogweb/web/controller/SecurityAuditDetailController.java index 2f4662a5..72e3ecae 100644 --- a/omod/src/main/java/org/openmrs/module/auditlogweb/web/controller/SecurityAuditDetailController.java +++ b/omod/src/main/java/org/openmrs/module/auditlogweb/web/controller/SecurityAuditDetailController.java @@ -48,7 +48,7 @@ public class SecurityAuditDetailController { /** * Display security audit event details. - * + * * @param request HTTP request * @param model model map for JSP * @return ModelAndView pointing to viewSecurityAudit.jsp @@ -83,7 +83,7 @@ public ModelAndView showDetails(HttpServletRequest request, ModelMap model) { // Fetch related events from the same session List relatedEvents = null; if (event.getSessionId() != null && !event.getSessionId().isEmpty()) { - relatedEvents = auditService.getRelatedSecurityEvents(event.getSessionId(), RELATED_EVENTS_LIMIT); + relatedEvents = auditService.getRelatedSecurityEvents(event.getSessionId(), 0, RELATED_EVENTS_LIMIT); } model.addAttribute("event", event); diff --git a/omod/src/test/java/org/openmrs/module/auditlogweb/rest/ReadAuditRestControllerTest.java b/omod/src/test/java/org/openmrs/module/auditlogweb/rest/ReadAuditRestControllerTest.java index 1b4cf6b0..a17860ee 100644 --- a/omod/src/test/java/org/openmrs/module/auditlogweb/rest/ReadAuditRestControllerTest.java +++ b/omod/src/test/java/org/openmrs/module/auditlogweb/rest/ReadAuditRestControllerTest.java @@ -134,7 +134,7 @@ public void shouldFetchRelatedAuditsSuccessfully() throws Exception { when(readAuditService.countRelatedReadLogs("session-123")).thenReturn(1L); when(readAuditService.mapToReadAuditLogDTO(relatedList)).thenReturn(Collections.emptyList()); - mockMvc.perform(get("/rest/v1/readauditlogs/releatedAudits").param("sessionId", "session-123").param("page", "0") + mockMvc.perform(get("/rest/v1/readauditlogs/relatedAudits").param("sessionId", "session-123").param("page", "0") .param("size", "10")).andExpect(status().isOk()); verify(readAuditService).getRelatedReadLogs("session-123", 0, 10); diff --git a/omod/src/test/java/org/openmrs/module/auditlogweb/rest/SecurityAuditRestControllerTest.java b/omod/src/test/java/org/openmrs/module/auditlogweb/rest/SecurityAuditRestControllerTest.java new file mode 100644 index 00000000..006a95fc --- /dev/null +++ b/omod/src/test/java/org/openmrs/module/auditlogweb/rest/SecurityAuditRestControllerTest.java @@ -0,0 +1,221 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under + * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. + * + * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS + * graphic logo is a trademark of OpenMRS Inc. + */ +package org.openmrs.module.auditlogweb.rest; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.openmrs.api.APIAuthenticationException; +import org.openmrs.api.context.Context; +import org.openmrs.module.auditlogweb.AuditSecurityEvent; +import org.openmrs.module.auditlogweb.api.AuditService; +import org.openmrs.module.auditlogweb.api.utils.AuditSecurityEventType; +import org.openmrs.module.auditlogweb.rest.exceptions.RestExceptionHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mockStatic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class SecurityAuditRestControllerTest { + + private MockMvc mockMvc; + + @Mock + private AuditService auditService; + + @InjectMocks + private SecurityAuditRestController securityAuditRestController; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + mockMvc = MockMvcBuilders.standaloneSetup(securityAuditRestController) + .setControllerAdvice(new RestExceptionHandler()).build(); + } + + @Test + public void shouldFetchSecurityAuditsSuccessfullyWithoutFilter() throws Exception { + when(auditService.getSecurityEvents(null, null, null, null, 0, 15)).thenReturn(Collections.emptyList()); + when(auditService.countSecurityEvents(null, null, null, null)).thenReturn(0L); + + mockMvc.perform(get("/rest/v1/securityauditlogs")).andExpect(status().isOk()); + + verify(auditService).getSecurityEvents(null, null, null, null, 0, 15); + verify(auditService).countSecurityEvents(null, null, null, null); + } + + @Test + public void shouldFetchSecurityAuditsSuccessfullyWithIdFilter() throws Exception { + AuditSecurityEvent event = buildAuditSecurityEvent(); + when(auditService.getSecurityEventById(1)).thenReturn(event); + + mockMvc.perform(get("/rest/v1/securityauditlogs").param("logId", "1")).andExpect(status().isOk()) + .andExpect(jsonPath("$.securityAuditLogs[0].id", is(1))) + .andExpect(jsonPath("$.securityAuditLogs[0].eventType", is("LOGIN_SUCCESS"))) + .andExpect(jsonPath("$.securityAuditLogs[0].username", is("admin"))) + .andExpect(jsonPath("$.securityAuditLogs[0].userUuid", is("user-uuid-1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].ipAddress", is("127.0.0.1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].userAgent", is("user-agent-1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].sessionId", is("session-123"))) + .andExpect(jsonPath("$.securityAuditLogs[0].details", is("{}"))).andExpect(jsonPath("$.totalLogs", is(1))) + .andExpect(jsonPath("$.totalPages", is(1))).andExpect(jsonPath("$.currentPage", is(0))); + + verify(auditService).getSecurityEventById(1); + } + + @Test + public void shouldThrowErrorIfInvalidLogIdPassed() throws Exception { + mockMvc.perform(get("/rest/v1/securityauditlogs").param("logId", "-1")).andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error", is("Bad Request"))) + .andExpect(jsonPath("$.message", is("Please provide a valid log ID"))); + } + + @Test + public void shouldThrowNotFoundErrorIfLogNotFoundForId() throws Exception { + when(auditService.getSecurityEventById(anyInt())).thenReturn(null); + mockMvc.perform(get("/rest/v1/securityauditlogs").param("logId", "2121")).andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error", is("Not Found"))) + .andExpect(jsonPath("$.message", is("No log found for this logId"))); + } + + @Test + public void shouldThrowErrorIfInvalidEventTypePassed() throws Exception { + mockMvc.perform(get("/rest/v1/securityauditlogs").param("eventType", "LOGIN_SUCESS")) + .andExpect(status().isBadRequest()).andExpect(jsonPath("$.error", is("Bad Request"))) + .andExpect(jsonPath("$.message", is("Invalid eventType LOGIN_SUCESS"))); + } + + @Test + public void shouldReturnStatusOkIfEmptyEventTypePassed() throws Exception { + mockMvc.perform(get("/rest/v1/securityauditlogs").param("eventType", "")).andExpect(status().isOk()); + } + + @Test + public void shouldFetchSecurityAuditsSuccessfullyWithEventTypeFilter() throws Exception { + AuditSecurityEvent event = buildAuditSecurityEvent(); + List eventList = Collections.singletonList(event); + + when(auditService.getSecurityEvents("LOGIN_SUCCESS", null, null, null, 0, 15)).thenReturn(eventList); + when(auditService.countSecurityEvents("LOGIN_SUCCESS", null, null, null)).thenReturn(1L); + + mockMvc.perform(get("/rest/v1/securityauditlogs").param("eventType", "LOGIN_SUCCESS")).andExpect(status().isOk()) + .andExpect(jsonPath("$.securityAuditLogs[0].id", is(1))) + .andExpect(jsonPath("$.securityAuditLogs[0].eventType", is("LOGIN_SUCCESS"))) + .andExpect(jsonPath("$.securityAuditLogs[0].username", is("admin"))) + .andExpect(jsonPath("$.securityAuditLogs[0].userUuid", is("user-uuid-1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].ipAddress", is("127.0.0.1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].userAgent", is("user-agent-1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].sessionId", is("session-123"))) + .andExpect(jsonPath("$.securityAuditLogs[0].details", is("{}"))).andExpect(jsonPath("$.totalLogs", is(1))) + .andExpect(jsonPath("$.currentLogs", is(1))).andExpect(jsonPath("$.totalPages", is(1))) + .andExpect(jsonPath("$.currentPage", is(0))); + + verify(auditService).getSecurityEvents("LOGIN_SUCCESS", null, null, null, 0, 15); + verify(auditService).countSecurityEvents("LOGIN_SUCCESS", null, null, null); + } + + @Test + public void shouldFetchSecurityAuditsSuccessfullyWithUserNameFilter() throws Exception { + AuditSecurityEvent event = buildAuditSecurityEvent(); + List eventList = Collections.singletonList(event); + + when(auditService.getSecurityEvents(null, "admin", null, null, 0, 15)).thenReturn(eventList); + when(auditService.countSecurityEvents(null, "admin", null, null)).thenReturn(1L); + + mockMvc.perform(get("/rest/v1/securityauditlogs").param("username", "admin")).andExpect(status().isOk()) + .andExpect(jsonPath("$.securityAuditLogs[0].id", is(1))) + .andExpect(jsonPath("$.securityAuditLogs[0].eventType", is("LOGIN_SUCCESS"))) + .andExpect(jsonPath("$.securityAuditLogs[0].username", is("admin"))) + .andExpect(jsonPath("$.securityAuditLogs[0].userUuid", is("user-uuid-1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].ipAddress", is("127.0.0.1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].userAgent", is("user-agent-1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].sessionId", is("session-123"))) + .andExpect(jsonPath("$.securityAuditLogs[0].details", is("{}"))).andExpect(jsonPath("$.totalLogs", is(1))) + .andExpect(jsonPath("$.currentLogs", is(1))).andExpect(jsonPath("$.totalPages", is(1))) + .andExpect(jsonPath("$.currentPage", is(0))); + + verify(auditService).getSecurityEvents(null, "admin", null, null, 0, 15); + verify(auditService).countSecurityEvents(null, "admin", null, null); + } + + @Test + public void shouldThrowErrorIfInvalidDatePassed() throws Exception { + mockMvc.perform(get("/rest/v1/securityauditlogs").param("startDate", "31/02/2025")) + .andExpect(status().isBadRequest()).andExpect(jsonPath("$.error", is("Bad Request"))).andExpect(jsonPath( + "$.message", is("Invalid month date or date format: '31/02/2025'. Expected format: DD/MM/YYYY"))); + } + + @Test + public void shouldFetchRelatedAuditsSuccessfully() throws Exception { + AuditSecurityEvent event = buildAuditSecurityEvent(); + List relatedList = Collections.singletonList(event); + + when(auditService.getRelatedSecurityEvents("session-123", 0, 10)).thenReturn(relatedList); + when(auditService.countRelatedSecurityEvents("session-123")).thenReturn(1L); + + mockMvc.perform(get("/rest/v1/securityauditlogs/relatedAudits").param("sessionId", "session-123").param("page", "0") + .param("size", "10")).andExpect(status().isOk()).andExpect(jsonPath("$.securityAuditLogs[0].id", is(1))) + .andExpect(jsonPath("$.securityAuditLogs[0].eventType", is("LOGIN_SUCCESS"))) + .andExpect(jsonPath("$.securityAuditLogs[0].username", is("admin"))) + .andExpect(jsonPath("$.securityAuditLogs[0].userUuid", is("user-uuid-1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].ipAddress", is("127.0.0.1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].userAgent", is("user-agent-1"))) + .andExpect(jsonPath("$.securityAuditLogs[0].sessionId", is("session-123"))) + .andExpect(jsonPath("$.securityAuditLogs[0].details", is("{}"))).andExpect(jsonPath("$.totalLogs", is(1))) + .andExpect(jsonPath("$.currentLogs", is(1))).andExpect(jsonPath("$.totalPages", is(1))) + .andExpect(jsonPath("$.currentPage", is(0))); + + verify(auditService).getRelatedSecurityEvents("session-123", 0, 10); + verify(auditService).countRelatedSecurityEvents("session-123"); + } + + @Test + public void shouldThrowUnauthorizedErrorIfNotAuthenticated() throws Exception { + when(auditService.getSecurityEvents(isNull(), isNull(), isNull(), isNull(), anyInt(), anyInt())) + .thenThrow(new APIAuthenticationException("Privileges required: View Security Audit Logs")); + try (MockedStatic ctx = mockStatic(Context.class)) { + ctx.when(Context::isAuthenticated).thenReturn(false); + mockMvc.perform(get("/rest/v1/securityauditlogs")).andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.error", is("Unauthorized"))); + } + } + + @Test + public void shouldThrowForbiddenErrorIfNotHasPrivileged() throws Exception { + when(auditService.getSecurityEvents(isNull(), isNull(), isNull(), isNull(), anyInt(), anyInt())) + .thenThrow(new APIAuthenticationException("Privileges required: View Security Audit Logs")); + try (MockedStatic ctx = mockStatic(Context.class)) { + ctx.when(Context::isAuthenticated).thenReturn(true); + mockMvc.perform(get("/rest/v1/securityauditlogs")).andExpect(status().isForbidden()) + .andExpect(jsonPath("$.error", is("Forbidden"))); + } + } + + private AuditSecurityEvent buildAuditSecurityEvent() { + return AuditSecurityEvent.builder().id(1).eventType(AuditSecurityEventType.LOGIN_SUCCESS).username("admin") + .userUuid("user-uuid-1").eventTime(new Date()).ipAddress("127.0.0.1").userAgent("user-agent-1") + .sessionId("session-123").details("{}").build(); + } +} diff --git a/omod/src/test/java/org/openmrs/module/auditlogweb/web/controller/SecurityAuditDetailControllerTest.java b/omod/src/test/java/org/openmrs/module/auditlogweb/web/controller/SecurityAuditDetailControllerTest.java index 142ddf34..af9c69fd 100644 --- a/omod/src/test/java/org/openmrs/module/auditlogweb/web/controller/SecurityAuditDetailControllerTest.java +++ b/omod/src/test/java/org/openmrs/module/auditlogweb/web/controller/SecurityAuditDetailControllerTest.java @@ -103,7 +103,7 @@ void shouldLoadEventDetailsWithSessionAndRelatedEvents() throws Exception { when(mockEvent.getSessionId()).thenReturn("session-test"); when(auditService.getSecurityEventById(2)).thenReturn(mockEvent); - when(auditService.getRelatedSecurityEvents("session-test", 10)).thenReturn(relatedList); + when(auditService.getRelatedSecurityEvents("session-test", 0, 10)).thenReturn(relatedList); mockMvc.perform(get("/module/auditlogweb/viewSecurityAudit.form").param("eventId", "2")).andExpect(status().isOk()) .andExpect(view().name("/module/auditlogweb/viewSecurityAudit")) @@ -111,7 +111,7 @@ void shouldLoadEventDetailsWithSessionAndRelatedEvents() throws Exception { .andExpect(model().attribute("page", "securityauditlogs")); verify(auditService).getSecurityEventById(2); - verify(auditService).getRelatedSecurityEvents("session-test", 10); + verify(auditService).getRelatedSecurityEvents("session-test", 0, 10); } @Test