-
Notifications
You must be signed in to change notification settings - Fork 20
AUDIT-61: Create the REST APIs for the Security Audit Logging #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6683ad7
86e653b
41a382d
5206f02
f6ac8f8
850b6cf
ff7ecce
09a7ad0
6a26eb7
f012ec3
4444444
2d126c5
fe17e13
da812af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<SecurityAuditLogDTO> securityAuditLogs; | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is worth fixing but I don't think it blocks the PR: a caller who lacks I checked that with this controller and that advice in a standalone MockMvc setup. For comparison, webservices.rest's own An
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done, added it |
||
|
|
||
| @GetMapping | ||
| public SecurityLogResponseDTO fetchSecurityAudits(@RequestParam(value = "logId", required = false) Integer logId, | ||
| @RequestParam(value = "eventType", required = false) String eventType, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a blocker, but a misspelled This method already rejects a malformed
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed, checking this early now |
||
| @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<SecurityAuditLogDTO> 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; | ||
| } | ||
|
Comment on lines
+69
to
+74
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be better to add validation directly to the request parameters, such as
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually using these annotations completely flips the situation means here if page is negative then we are setting it to 0 by default and if size <= 0 then take as 15 by default but these annotations will just validate these values and if not verifies then throw the exception or simple the error body instead of the default results we returning currently. Second these annotations will probably not the best case if we need to throw the bad request body because first we need to register the the |
||
|
|
||
| Date start = UtilClass.parseDate(startDate, false); | ||
| Date end = UtilClass.parseDate(endDate, true); | ||
|
|
||
| List<AuditSecurityEvent> securityEvents = auditService.getSecurityEvents(eventType, username, start, end, page, | ||
| size); | ||
| long totalCount = auditService.countSecurityEvents(eventType, username, start, end); | ||
| int totalPages = UtilClass.computeTotalPages(totalCount, size); | ||
|
|
||
| List<SecurityAuditLogDTO> 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<AuditSecurityEvent> allRelated = auditService.getRelatedSecurityEvents(sessionId, page, size); | ||
| long totalCount = auditService.countRelatedSecurityEvents(sessionId); | ||
| int totalPages = UtilClass.computeTotalPages(totalCount, size); | ||
|
|
||
| List<SecurityAuditLogDTO> 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<SecurityAuditLogDTO> mapToDTOs(List<AuditSecurityEvent> events) { | ||
| if (events == null) { | ||
| return Collections.emptyList(); | ||
| } | ||
| List<SecurityAuditLogDTO> dtos = new ArrayList<>(events.size()); | ||
| for (AuditSecurityEvent event : events) { | ||
| dtos.add(mapToDTO(event)); | ||
| } | ||
| return dtos; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<Map<String, String>> handleAuditLogUnavailable(AuditLogUna | |||||||||||||||||||||||
| return buildResponseEntity("Audit Log Unavailable", ex.getMessage(), HttpStatus.SERVICE_UNAVAILABLE); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @ExceptionHandler(APIAuthenticationException.class) | ||||||||||||||||||||||||
| public ResponseEntity<Map<String, String>> handleAPIAuthException(APIAuthenticationException ex) { | ||||||||||||||||||||||||
| if (Context.isAuthenticated()) { | ||||||||||||||||||||||||
| return buildResponseEntity("Forbidden", ex.getMessage(), HttpStatus.FORBIDDEN); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return buildResponseEntity("Unauthorized", ex.getMessage(), HttpStatus.UNAUTHORIZED); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+89
to
+95
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An unauthenticated caller reaches this handler too, and for that case the platform answers 401 rather than 403. In webservices.rest 2.49.0,
Suggested change
That needs an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, fixed ! |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @ExceptionHandler(Exception.class) | ||||||||||||||||||||||||
| public ResponseEntity<Map<String, String>> handleGeneralError(Exception ex) { | ||||||||||||||||||||||||
| return buildResponseEntity("Internal Server Error", "An unexpected error occurred", | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currentPageis already available in the pagination context. Is it necessary to send it back to the client?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that would be redundant if the client already sets the page parameter in the request then we are going to return same on the response too, but if client not sets page parameter then in that case it would be sightly useful in the response the client will have clear picture of from which page this logs belongs too. So it guess it's not that unnecessary, otherwise we can remove it.