mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-22 04:09:22 +00:00
feat(audit): introduce structured Audit API with export, stats, and cleanup endpoints (#4217)
# Description of Changes - Added new REST-based `AuditDashboardController` under `/api/v1/audit` with endpoints for: - Audit data retrieval with pagination (`/data`) - Statistics retrieval (`/stats`) - Export in CSV and JSON (`/export/csv`, `/export/json`) - Cleanup of audit events before a given date (`/cleanup/before`) - Retrieval of distinct audit event types (`/types`) - Extracted web dashboard logic into `AuditDashboardWebController` (view rendering only). - Introduced new API models: - `AuditDataRequest`, `AuditDataResponse` - `AuditExportRequest`, `AuditDateExportRequest` - `AuditStatsResponse` - Extended `PersistentAuditEventRepository` with richer query methods (histograms, counts, top/latest events, distinct principals). - Updated `dashboard.js` to use new API endpoints under `/api/v1/audit`. - Enhanced authentication handlers and user endpoints with `@Audited` annotations for login/logout/password change events. - Cleaned up `LicenseKeyChecker` by removing unused `updateLicenseKey` method. - Moved admin-related controllers into `controller.api` namespace with proper OpenAPI annotations (`@Operation`, `@Tag`). - Improved `CleanUrlInterceptor` whitelist for new query parameters (`days`, `date`). --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
parent
d23c2eaa30
commit
28b1b96cfb
@ -20,6 +20,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
"endpoints",
|
"endpoints",
|
||||||
"logout",
|
"logout",
|
||||||
"error",
|
"error",
|
||||||
|
"days",
|
||||||
|
"date",
|
||||||
"errorOAuth",
|
"errorOAuth",
|
||||||
"file",
|
"file",
|
||||||
"messageType",
|
"messageType",
|
||||||
|
@ -10,8 +10,12 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -27,6 +31,8 @@ import stirling.software.common.service.TaskManager;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "Job Management", description = "Job Management API")
|
||||||
public class JobController {
|
public class JobController {
|
||||||
|
|
||||||
private final TaskManager taskManager;
|
private final TaskManager taskManager;
|
||||||
@ -40,7 +46,8 @@ public class JobController {
|
|||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @return The job result
|
* @return The job result
|
||||||
*/
|
*/
|
||||||
@GetMapping("/api/v1/general/job/{jobId}")
|
@GetMapping("/job/{jobId}")
|
||||||
|
@Operation(summary = "Get job status")
|
||||||
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> getJobStatus(@PathVariable("jobId") String jobId) {
|
||||||
JobResult result = taskManager.getJobResult(jobId);
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@ -68,7 +75,8 @@ public class JobController {
|
|||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @return The job result
|
* @return The job result
|
||||||
*/
|
*/
|
||||||
@GetMapping("/api/v1/general/job/{jobId}/result")
|
@GetMapping("/job/{jobId}/result")
|
||||||
|
@Operation(summary = "Get job result")
|
||||||
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> getJobResult(@PathVariable("jobId") String jobId) {
|
||||||
JobResult result = taskManager.getJobResult(jobId);
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@ -130,7 +138,8 @@ public class JobController {
|
|||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @return Response indicating whether the job was cancelled
|
* @return Response indicating whether the job was cancelled
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/api/v1/general/job/{jobId}")
|
@DeleteMapping("/job/{jobId}")
|
||||||
|
@Operation(summary = "Cancel a job")
|
||||||
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> cancelJob(@PathVariable("jobId") String jobId) {
|
||||||
log.debug("Request to cancel job: {}", jobId);
|
log.debug("Request to cancel job: {}", jobId);
|
||||||
|
|
||||||
@ -197,7 +206,8 @@ public class JobController {
|
|||||||
* @param jobId The job ID
|
* @param jobId The job ID
|
||||||
* @return List of files for the job
|
* @return List of files for the job
|
||||||
*/
|
*/
|
||||||
@GetMapping("/api/v1/general/job/{jobId}/result/files")
|
@GetMapping("/job/{jobId}/result/files")
|
||||||
|
@Operation(summary = "Get job result files")
|
||||||
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
|
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
|
||||||
JobResult result = taskManager.getJobResult(jobId);
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@ -226,7 +236,8 @@ public class JobController {
|
|||||||
* @param fileId The file ID
|
* @param fileId The file ID
|
||||||
* @return The file metadata
|
* @return The file metadata
|
||||||
*/
|
*/
|
||||||
@GetMapping("/api/v1/general/files/{fileId}/metadata")
|
@GetMapping("/files/{fileId}/metadata")
|
||||||
|
@Operation(summary = "Get file metadata")
|
||||||
public ResponseEntity<?> getFileMetadata(@PathVariable("fileId") String fileId) {
|
public ResponseEntity<?> getFileMetadata(@PathVariable("fileId") String fileId) {
|
||||||
try {
|
try {
|
||||||
// Verify file exists
|
// Verify file exists
|
||||||
@ -266,7 +277,8 @@ public class JobController {
|
|||||||
* @param fileId The file ID
|
* @param fileId The file ID
|
||||||
* @return The file content
|
* @return The file content
|
||||||
*/
|
*/
|
||||||
@GetMapping("/api/v1/general/files/{fileId}")
|
@GetMapping("/files/{fileId}")
|
||||||
|
@Operation(summary = "Download a file")
|
||||||
public ResponseEntity<?> downloadFile(@PathVariable("fileId") String fileId) {
|
public ResponseEntity<?> downloadFile(@PathVariable("fileId") String fileId) {
|
||||||
try {
|
try {
|
||||||
// Verify file exists
|
// Verify file exists
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package stirling.software.proprietary.controller;
|
package stirling.software.proprietary.controller.api;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -6,8 +6,12 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@ -22,6 +26,9 @@ import stirling.software.common.service.TaskManager;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequestMapping("/api/v1/admin")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@Tag(name = "Admin Job Management", description = "Admin-only Job Management APIs")
|
||||||
public class AdminJobController {
|
public class AdminJobController {
|
||||||
|
|
||||||
private final TaskManager taskManager;
|
private final TaskManager taskManager;
|
||||||
@ -32,7 +39,8 @@ public class AdminJobController {
|
|||||||
*
|
*
|
||||||
* @return Job statistics
|
* @return Job statistics
|
||||||
*/
|
*/
|
||||||
@GetMapping("/api/v1/admin/job/stats")
|
@GetMapping("/job/stats")
|
||||||
|
@Operation(summary = "Get job statistics")
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
public ResponseEntity<JobStats> getJobStats() {
|
public ResponseEntity<JobStats> getJobStats() {
|
||||||
JobStats stats = taskManager.getJobStats();
|
JobStats stats = taskManager.getJobStats();
|
||||||
@ -48,7 +56,8 @@ public class AdminJobController {
|
|||||||
*
|
*
|
||||||
* @return Queue statistics
|
* @return Queue statistics
|
||||||
*/
|
*/
|
||||||
@GetMapping("/api/v1/admin/job/queue/stats")
|
@GetMapping("/job/queue/stats")
|
||||||
|
@Operation(summary = "Get job queue statistics")
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
public ResponseEntity<?> getQueueStats() {
|
public ResponseEntity<?> getQueueStats() {
|
||||||
Map<String, Object> queueStats = jobQueue.getQueueStats();
|
Map<String, Object> queueStats = jobQueue.getQueueStats();
|
||||||
@ -61,7 +70,8 @@ public class AdminJobController {
|
|||||||
*
|
*
|
||||||
* @return A response indicating how many jobs were cleaned up
|
* @return A response indicating how many jobs were cleaned up
|
||||||
*/
|
*/
|
||||||
@PostMapping("/api/v1/admin/job/cleanup")
|
@PostMapping("/job/cleanup")
|
||||||
|
@Operation(summary = "Cleanup old jobs")
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
public ResponseEntity<?> cleanupOldJobs() {
|
public ResponseEntity<?> cleanupOldJobs() {
|
||||||
int beforeCount = taskManager.getJobStats().getTotalJobs();
|
int beforeCount = taskManager.getJobStats().getTotalJobs();
|
@ -1,4 +1,4 @@
|
|||||||
package stirling.software.proprietary.controller;
|
package stirling.software.proprietary.controller.api;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@ -6,13 +6,13 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springdoc.core.annotations.ParameterObject;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@ -22,133 +22,101 @@ import org.springframework.http.HttpHeaders;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.proprietary.audit.AuditEventType;
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
import stirling.software.proprietary.audit.AuditLevel;
|
import stirling.software.proprietary.model.api.audit.AuditDataRequest;
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
import stirling.software.proprietary.model.api.audit.AuditDataResponse;
|
||||||
|
import stirling.software.proprietary.model.api.audit.AuditExportRequest;
|
||||||
|
import stirling.software.proprietary.model.api.audit.AuditStatsResponse;
|
||||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
/** Controller for the audit dashboard. Admin-only access. */
|
/** REST endpoints for the audit dashboard. */
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Controller
|
@RestController
|
||||||
@RequestMapping("/audit")
|
@RequestMapping("/api/v1/audit")
|
||||||
@PreAuthorize("hasRole('ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@EnterpriseEndpoint
|
@EnterpriseEndpoint
|
||||||
|
@Tag(name = "Audit", description = "Only Enterprise - Audit related operations")
|
||||||
public class AuditDashboardController {
|
public class AuditDashboardController {
|
||||||
|
|
||||||
private final PersistentAuditEventRepository auditRepository;
|
private final PersistentAuditEventRepository auditRepository;
|
||||||
private final AuditConfigurationProperties auditConfig;
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
/** Display the audit dashboard. */
|
|
||||||
@GetMapping
|
|
||||||
public String showDashboard(Model model) {
|
|
||||||
model.addAttribute("auditEnabled", auditConfig.isEnabled());
|
|
||||||
model.addAttribute("auditLevel", auditConfig.getAuditLevel());
|
|
||||||
model.addAttribute("auditLevelInt", auditConfig.getLevel());
|
|
||||||
model.addAttribute("retentionDays", auditConfig.getRetentionDays());
|
|
||||||
|
|
||||||
// Add audit level enum values for display
|
|
||||||
model.addAttribute("auditLevels", AuditLevel.values());
|
|
||||||
|
|
||||||
// Add audit event types for the dropdown
|
|
||||||
model.addAttribute("auditEventTypes", AuditEventType.values());
|
|
||||||
|
|
||||||
return "audit/dashboard";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get audit events data for the dashboard tables. */
|
/** Get audit events data for the dashboard tables. */
|
||||||
@GetMapping("/data")
|
@GetMapping("/data")
|
||||||
@ResponseBody
|
@Operation(summary = "Get audit events data")
|
||||||
public Map<String, Object> getAuditData(
|
public AuditDataResponse getAuditData(@ParameterObject AuditDataRequest request) {
|
||||||
@RequestParam(value = "page", defaultValue = "0") int page,
|
|
||||||
@RequestParam(value = "size", defaultValue = "30") int size,
|
|
||||||
@RequestParam(value = "type", required = false) String type,
|
|
||||||
@RequestParam(value = "principal", required = false) String principal,
|
|
||||||
@RequestParam(value = "startDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
|
||||||
LocalDate startDate,
|
|
||||||
@RequestParam(value = "endDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
|
||||||
LocalDate endDate,
|
|
||||||
HttpServletRequest request) {
|
|
||||||
|
|
||||||
Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").descending());
|
Pageable pageable =
|
||||||
|
PageRequest.of(
|
||||||
|
request.getPage(), request.getSize(), Sort.by("timestamp").descending());
|
||||||
Page<PersistentAuditEvent> events;
|
Page<PersistentAuditEvent> events;
|
||||||
|
|
||||||
String mode;
|
String type = request.getType();
|
||||||
|
String principal = request.getPrincipal();
|
||||||
|
LocalDate startDate = request.getStartDate();
|
||||||
|
LocalDate endDate = request.getEndDate();
|
||||||
|
|
||||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
if (type != null && principal != null && startDate != null && endDate != null) {
|
||||||
mode = "principal + type + startDate + endDate";
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
events =
|
events =
|
||||||
auditRepository.findByPrincipalAndTypeAndTimestampBetween(
|
auditRepository.findByPrincipalAndTypeAndTimestampBetween(
|
||||||
principal, type, start, end, pageable);
|
principal, type, start, end, pageable);
|
||||||
} else if (type != null && principal != null) {
|
} else if (type != null && principal != null) {
|
||||||
mode = "principal + type";
|
|
||||||
events = auditRepository.findByPrincipalAndType(principal, type, pageable);
|
events = auditRepository.findByPrincipalAndType(principal, type, pageable);
|
||||||
} else if (type != null && startDate != null && endDate != null) {
|
} else if (type != null && startDate != null && endDate != null) {
|
||||||
mode = "type + startDate + endDate";
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
events = auditRepository.findByTypeAndTimestampBetween(type, start, end, pageable);
|
events = auditRepository.findByTypeAndTimestampBetween(type, start, end, pageable);
|
||||||
} else if (principal != null && startDate != null && endDate != null) {
|
} else if (principal != null && startDate != null && endDate != null) {
|
||||||
mode = "principal + startDate + endDate";
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
events =
|
events =
|
||||||
auditRepository.findByPrincipalAndTimestampBetween(
|
auditRepository.findByPrincipalAndTimestampBetween(
|
||||||
principal, start, end, pageable);
|
principal, start, end, pageable);
|
||||||
} else if (startDate != null && endDate != null) {
|
} else if (startDate != null && endDate != null) {
|
||||||
mode = "startDate + endDate";
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
events = auditRepository.findByTimestampBetween(start, end, pageable);
|
events = auditRepository.findByTimestampBetween(start, end, pageable);
|
||||||
} else if (type != null) {
|
} else if (type != null) {
|
||||||
mode = "type";
|
|
||||||
events = auditRepository.findByType(type, pageable);
|
events = auditRepository.findByType(type, pageable);
|
||||||
} else if (principal != null) {
|
} else if (principal != null) {
|
||||||
mode = "principal";
|
|
||||||
events = auditRepository.findByPrincipal(principal, pageable);
|
events = auditRepository.findByPrincipal(principal, pageable);
|
||||||
} else {
|
} else {
|
||||||
mode = "all";
|
|
||||||
events = auditRepository.findAll(pageable);
|
events = auditRepository.findAll(pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
List<PersistentAuditEvent> content = events.getContent();
|
List<PersistentAuditEvent> content = events.getContent();
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
return new AuditDataResponse(
|
||||||
response.put("content", content);
|
content, events.getTotalPages(), events.getTotalElements(), events.getNumber());
|
||||||
response.put("totalPages", events.getTotalPages());
|
|
||||||
response.put("totalElements", events.getTotalElements());
|
|
||||||
response.put("currentPage", events.getNumber());
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get statistics for charts. */
|
/** Get statistics for charts (last X days). Existing behavior preserved. */
|
||||||
@GetMapping("/stats")
|
@GetMapping("/stats")
|
||||||
@ResponseBody
|
@Operation(summary = "Get audit statistics for the last N days")
|
||||||
public Map<String, Object> getAuditStats(
|
public AuditStatsResponse getAuditStats(
|
||||||
|
@Schema(description = "Number of days to look back for audit events", example = "7", required = true)
|
||||||
@RequestParam(value = "days", defaultValue = "7") int days) {
|
@RequestParam(value = "days", defaultValue = "7") int days) {
|
||||||
|
|
||||||
// Get events from the last X days
|
// Get events from the last X days
|
||||||
@ -181,18 +149,53 @@ public class AuditDashboardController {
|
|||||||
.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||||
Collectors.counting()));
|
Collectors.counting()));
|
||||||
|
|
||||||
Map<String, Object> stats = new HashMap<>();
|
return new AuditStatsResponse(eventsByType, eventsByPrincipal, eventsByDay, events.size());
|
||||||
stats.put("eventsByType", eventsByType);
|
|
||||||
stats.put("eventsByPrincipal", eventsByPrincipal);
|
|
||||||
stats.put("eventsByDay", eventsByDay);
|
|
||||||
stats.put("totalEvents", events.size());
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /** Advanced statistics using repository aggregations, with explicit date range. */
|
||||||
|
// @GetMapping("/stats/range")
|
||||||
|
// @Operation(summary = "Get audit statistics for a date range (aggregated in DB)")
|
||||||
|
// public Map<String, Object> getAuditStatsRange(@ParameterObject AuditDateExportRequest
|
||||||
|
// request) {
|
||||||
|
|
||||||
|
// LocalDate startDate = request.getStartDate();
|
||||||
|
// LocalDate endDate = request.getEndDate();
|
||||||
|
// Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
|
// Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
|
|
||||||
|
// Map<String, Long> byType = toStringLongMap(auditRepository.countByTypeBetween(start,
|
||||||
|
// end));
|
||||||
|
// Map<String, Long> byPrincipal =
|
||||||
|
// toStringLongMap(auditRepository.countByPrincipalBetween(start, end));
|
||||||
|
|
||||||
|
// Map<String, Long> byDay = new HashMap<>();
|
||||||
|
// for (Object[] row : auditRepository.histogramByDayBetween(start, end)) {
|
||||||
|
// int y = ((Number) row[0]).intValue();
|
||||||
|
// int m = ((Number) row[1]).intValue();
|
||||||
|
// int d = ((Number) row[2]).intValue();
|
||||||
|
// long count = ((Number) row[3]).longValue();
|
||||||
|
// String key = String.format("%04d-%02d-%02d", y, m, d);
|
||||||
|
// byDay.put(key, count);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Map<String, Long> byHour = new HashMap<>();
|
||||||
|
// for (Object[] row : auditRepository.histogramByHourBetween(start, end)) {
|
||||||
|
// int hour = ((Number) row[0]).intValue();
|
||||||
|
// long count = ((Number) row[1]).longValue();
|
||||||
|
// byHour.put(String.format("%02d:00", hour), count);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Map<String, Object> payload = new HashMap<>();
|
||||||
|
// payload.put("byType", byType);
|
||||||
|
// payload.put("byPrincipal", byPrincipal);
|
||||||
|
// payload.put("byDay", byDay);
|
||||||
|
// payload.put("byHour", byHour);
|
||||||
|
// return payload;
|
||||||
|
// }
|
||||||
|
|
||||||
/** Get all unique event types from the database for filtering. */
|
/** Get all unique event types from the database for filtering. */
|
||||||
@GetMapping("/types")
|
@GetMapping("/types")
|
||||||
@ResponseBody
|
@Operation(summary = "Get all unique audit event types")
|
||||||
public List<String> getAuditTypes() {
|
public List<String> getAuditTypes() {
|
||||||
// Get distinct event types from the database
|
// Get distinct event types from the database
|
||||||
List<String> dbTypes = auditRepository.findDistinctEventTypes();
|
List<String> dbTypes = auditRepository.findDistinctEventTypes();
|
||||||
@ -212,49 +215,11 @@ public class AuditDashboardController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Export audit data as CSV. */
|
/** Export audit data as CSV. */
|
||||||
@GetMapping("/export")
|
@GetMapping("/export/csv")
|
||||||
public ResponseEntity<byte[]> exportAuditData(
|
@Operation(summary = "Export audit data as CSV")
|
||||||
@RequestParam(value = "type", required = false) String type,
|
public ResponseEntity<byte[]> exportAuditData(@ParameterObject AuditExportRequest request) {
|
||||||
@RequestParam(value = "principal", required = false) String principal,
|
|
||||||
@RequestParam(value = "startDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
|
||||||
LocalDate startDate,
|
|
||||||
@RequestParam(value = "endDate", required = false)
|
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
|
||||||
LocalDate endDate) {
|
|
||||||
|
|
||||||
// Get data with same filtering as getAuditData
|
List<PersistentAuditEvent> events = getAuditEventsByCriteria(request);
|
||||||
List<PersistentAuditEvent> events;
|
|
||||||
|
|
||||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events =
|
|
||||||
auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
|
||||||
principal, type, start, end);
|
|
||||||
} else if (type != null && principal != null) {
|
|
||||||
events = auditRepository.findAllByPrincipalAndTypeForExport(principal, type);
|
|
||||||
} else if (type != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByTypeAndTimestampBetweenForExport(type, start, end);
|
|
||||||
} else if (principal != null && startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events =
|
|
||||||
auditRepository.findAllByPrincipalAndTimestampBetweenForExport(
|
|
||||||
principal, start, end);
|
|
||||||
} else if (startDate != null && endDate != null) {
|
|
||||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
|
||||||
events = auditRepository.findAllByTimestampBetweenForExport(start, end);
|
|
||||||
} else if (type != null) {
|
|
||||||
events = auditRepository.findByTypeForExport(type);
|
|
||||||
} else if (principal != null) {
|
|
||||||
events = auditRepository.findAllByPrincipalForExport(principal);
|
|
||||||
} else {
|
|
||||||
events = auditRepository.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to CSV
|
// Convert to CSV
|
||||||
StringBuilder csv = new StringBuilder();
|
StringBuilder csv = new StringBuilder();
|
||||||
@ -282,15 +247,113 @@ public class AuditDashboardController {
|
|||||||
|
|
||||||
/** Export audit data as JSON. */
|
/** Export audit data as JSON. */
|
||||||
@GetMapping("/export/json")
|
@GetMapping("/export/json")
|
||||||
public ResponseEntity<byte[]> exportAuditDataJson(
|
@Operation(summary = "Export audit data as JSON")
|
||||||
@RequestParam(value = "type", required = false) String type,
|
public ResponseEntity<byte[]> exportAuditDataJson(@ParameterObject AuditExportRequest request) {
|
||||||
@RequestParam(value = "principal", required = false) String principal,
|
|
||||||
@RequestParam(value = "startDate", required = false)
|
List<PersistentAuditEvent> events = getAuditEventsByCriteria(request);
|
||||||
|
|
||||||
|
// Convert to JSON
|
||||||
|
try {
|
||||||
|
byte[] jsonBytes = objectMapper.writeValueAsBytes(events);
|
||||||
|
|
||||||
|
// Set up HTTP headers for download
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||||
|
|
||||||
|
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Error serializing audit events to JSON", e);
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /** Get all unique principals. */
|
||||||
|
// @GetMapping("/principals")
|
||||||
|
// @Operation(summary = "Get all distinct principals")
|
||||||
|
// public List<String> getPrincipals() {
|
||||||
|
// return auditRepository.findDistinctPrincipals();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /** Get principals by event type. */
|
||||||
|
// @GetMapping("/types/{type}/principals")
|
||||||
|
// @Operation(summary = "Get distinct principals for a given type")
|
||||||
|
// public List<String> getPrincipalsByType(@PathVariable("type") String type) {
|
||||||
|
// return auditRepository.findDistinctPrincipalsByType(type);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /** Latest helpers */
|
||||||
|
// @GetMapping("/latest")
|
||||||
|
// @Operation(summary = "Get the latest audit event, optionally filtered by type or principal")
|
||||||
|
// public ResponseEntity<PersistentAuditEvent> getLatest(
|
||||||
|
// @RequestParam(value = "type", required = false) String type,
|
||||||
|
// @RequestParam(value = "principal", required = false) String principal) {
|
||||||
|
// if (type != null) {
|
||||||
|
// return auditRepository
|
||||||
|
// .findTopByTypeOrderByTimestampDesc(type)
|
||||||
|
// .map(ResponseEntity::ok)
|
||||||
|
// .orElse(ResponseEntity.noContent().build());
|
||||||
|
// } else if (principal != null) {
|
||||||
|
// return auditRepository
|
||||||
|
// .findTopByPrincipalOrderByTimestampDesc(principal)
|
||||||
|
// .map(ResponseEntity::ok)
|
||||||
|
// .orElse(ResponseEntity.noContent().build());
|
||||||
|
// }
|
||||||
|
// return auditRepository
|
||||||
|
// .findTopByOrderByTimestampDesc()
|
||||||
|
// .map(ResponseEntity::ok)
|
||||||
|
// .orElse(ResponseEntity.noContent().build());
|
||||||
|
// }
|
||||||
|
|
||||||
|
/** Cleanup endpoints data before a certain date */
|
||||||
|
@DeleteMapping("/cleanup/before")
|
||||||
|
@Operation(
|
||||||
|
summary = "Cleanup audit events before a certain date",
|
||||||
|
description = "Deletes all audit events before the specified date.")
|
||||||
|
public Map<String, Object> cleanupBefore(
|
||||||
|
@RequestParam(value = "date", required = true)
|
||||||
|
@Schema(
|
||||||
|
description = "The cutoff date for cleanup",
|
||||||
|
example = "2025-01-01",
|
||||||
|
format = "date")
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||||
LocalDate startDate,
|
LocalDate date) {
|
||||||
@RequestParam(value = "endDate", required = false)
|
if (date != null && !date.isAfter(LocalDate.now())) {
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
Instant cutoff = date.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||||
LocalDate endDate) {
|
int deleted = auditRepository.deleteByTimestampBefore(cutoff);
|
||||||
|
return Map.of("deleted", deleted, "cutoffDate", date.toString());
|
||||||
|
}
|
||||||
|
return Map.of(
|
||||||
|
"error",
|
||||||
|
"Invalid date format. Use ISO date format (YYYY-MM-DD). Date must be in the past.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// // ===== Helpers =====
|
||||||
|
|
||||||
|
// private Map<String, Long> toStringLongMap(List<Object[]> rows) {
|
||||||
|
// Map<String, Long> map = new HashMap<>();
|
||||||
|
// for (Object[] row : rows) {
|
||||||
|
// String key = String.valueOf(row[0]);
|
||||||
|
// long val = ((Number) row[1]).longValue();
|
||||||
|
// map.put(key, val);
|
||||||
|
// }
|
||||||
|
// return map;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/** Helper method to escape CSV fields. */
|
||||||
|
private String escapeCSV(String field) {
|
||||||
|
if (field == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// Replace double quotes with two double quotes and wrap in quotes
|
||||||
|
return "\"" + field.replace("\"", "\"\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PersistentAuditEvent> getAuditEventsByCriteria(AuditExportRequest request) {
|
||||||
|
String type = request.getType();
|
||||||
|
String principal = request.getPrincipal();
|
||||||
|
LocalDate startDate = request.getStartDate();
|
||||||
|
LocalDate endDate = request.getEndDate();
|
||||||
|
|
||||||
// Get data with same filtering as getAuditData
|
// Get data with same filtering as getAuditData
|
||||||
List<PersistentAuditEvent> events;
|
List<PersistentAuditEvent> events;
|
||||||
@ -324,29 +387,6 @@ public class AuditDashboardController {
|
|||||||
} else {
|
} else {
|
||||||
events = auditRepository.findAll();
|
events = auditRepository.findAll();
|
||||||
}
|
}
|
||||||
|
return events;
|
||||||
// Convert to JSON
|
|
||||||
try {
|
|
||||||
byte[] jsonBytes = objectMapper.writeValueAsBytes(events);
|
|
||||||
|
|
||||||
// Set up HTTP headers for download
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
|
||||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
|
||||||
|
|
||||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
|
||||||
} catch (JsonProcessingException e) {
|
|
||||||
log.error("Error serializing audit events to JSON", e);
|
|
||||||
return ResponseEntity.internalServerError().build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Helper method to escape CSV fields. */
|
|
||||||
private String escapeCSV(String field) {
|
|
||||||
if (field == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
// Replace double quotes with two double quotes and wrap in quotes
|
|
||||||
return "\"" + field.replace("\"", "\"\"") + "\"";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package stirling.software.proprietary.controller.web;
|
||||||
|
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||||
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@EnterpriseEndpoint
|
||||||
|
public class AuditDashboardWebController {
|
||||||
|
private final AuditConfigurationProperties auditConfig;
|
||||||
|
|
||||||
|
/** Display the audit dashboard. */
|
||||||
|
@GetMapping("/audit")
|
||||||
|
@Hidden
|
||||||
|
public String showDashboard(Model model) {
|
||||||
|
model.addAttribute("auditEnabled", auditConfig.isEnabled());
|
||||||
|
model.addAttribute("auditLevel", auditConfig.getAuditLevel());
|
||||||
|
model.addAttribute("auditLevelInt", auditConfig.getLevel());
|
||||||
|
model.addAttribute("retentionDays", auditConfig.getRetentionDays());
|
||||||
|
|
||||||
|
// Add audit level enum values for display
|
||||||
|
model.addAttribute("auditLevels", AuditLevel.values());
|
||||||
|
|
||||||
|
// Add audit event types for the dropdown
|
||||||
|
model.addAttribute("auditEventTypes", AuditEventType.values());
|
||||||
|
|
||||||
|
return "audit/dashboard";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package stirling.software.proprietary.model.api.audit;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
/** Request object used for querying audit events. */
|
||||||
|
@Data
|
||||||
|
@EnterpriseEndpoint
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class AuditDataRequest extends AuditExportRequest {
|
||||||
|
|
||||||
|
@Schema(description = "Page number for pagination", example = "0", defaultValue = "0")
|
||||||
|
private int page = 0;
|
||||||
|
|
||||||
|
@Schema(description = "Page size for pagination", example = "30", defaultValue = "30")
|
||||||
|
private int size = 30;
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package stirling.software.proprietary.model.api.audit;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||||
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
/** Response object returned when querying audit data. */
|
||||||
|
@Data
|
||||||
|
@EnterpriseEndpoint
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class AuditDataResponse {
|
||||||
|
|
||||||
|
@Schema(description = "List of audit events matching the query")
|
||||||
|
private List<PersistentAuditEvent> content;
|
||||||
|
|
||||||
|
@Schema(description = "Total number of pages available", example = "5")
|
||||||
|
private int totalPages;
|
||||||
|
|
||||||
|
@Schema(description = "Total number of events", example = "150")
|
||||||
|
private long totalElements;
|
||||||
|
|
||||||
|
@Schema(description = "Current page index", example = "0")
|
||||||
|
private int currentPage;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package stirling.software.proprietary.model.api.audit;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EnterpriseEndpoint
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class AuditDateExportRequest {
|
||||||
|
|
||||||
|
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||||
|
@Schema(description = "Start date for the export range", example = "2025-01-01")
|
||||||
|
private LocalDate startDate;
|
||||||
|
|
||||||
|
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||||
|
@Schema(description = "End date for the export range", example = "2025-12-31")
|
||||||
|
private LocalDate endDate;
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package stirling.software.proprietary.model.api.audit;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
/** Request object used for exporting audit data with filters. */
|
||||||
|
@Data
|
||||||
|
@EnterpriseEndpoint
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class AuditExportRequest extends AuditDateExportRequest {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Audit event type to filter by",
|
||||||
|
example = "USER_LOGIN",
|
||||||
|
allowableValues = {
|
||||||
|
"USER_LOGIN",
|
||||||
|
"USER_LOGOUT",
|
||||||
|
"USER_FAILED_LOGIN",
|
||||||
|
"USER_PROFILE_UPDATE",
|
||||||
|
"SETTINGS_CHANGED",
|
||||||
|
"FILE_OPERATION",
|
||||||
|
"PDF_PROCESS",
|
||||||
|
"HTTP_REQUEST"
|
||||||
|
})
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "Principal (username) to filter by", example = "admin")
|
||||||
|
private String principal;
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package stirling.software.proprietary.model.api.audit;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||||
|
|
||||||
|
/** Response object for audit statistics. */
|
||||||
|
@Data
|
||||||
|
@EnterpriseEndpoint
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class AuditStatsResponse {
|
||||||
|
|
||||||
|
@Schema(description = "Count of events grouped by type")
|
||||||
|
private Map<String, Long> eventsByType;
|
||||||
|
|
||||||
|
@Schema(description = "Count of events grouped by principal")
|
||||||
|
private Map<String, Long> eventsByPrincipal;
|
||||||
|
|
||||||
|
@Schema(description = "Count of events grouped by day")
|
||||||
|
private Map<String, Long> eventsByDay;
|
||||||
|
|
||||||
|
@Schema(description = "Total number of events in the period", example = "42")
|
||||||
|
private int totalEvents;
|
||||||
|
}
|
@ -2,6 +2,7 @@ package stirling.software.proprietary.repository;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
@ -19,7 +20,8 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
|||||||
|
|
||||||
// Basic queries
|
// Basic queries
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||||
|
+ " :principal, '%'))")
|
||||||
Page<PersistentAuditEvent> findByPrincipal(
|
Page<PersistentAuditEvent> findByPrincipal(
|
||||||
@Param("principal") String principal, Pageable pageable);
|
@Param("principal") String principal, Pageable pageable);
|
||||||
|
|
||||||
@ -29,12 +31,14 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
|||||||
Instant startDate, Instant endDate, Pageable pageable);
|
Instant startDate, Instant endDate, Pageable pageable);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||||
|
+ " :principal, '%')) AND e.type = :type")
|
||||||
Page<PersistentAuditEvent> findByPrincipalAndType(
|
Page<PersistentAuditEvent> findByPrincipalAndType(
|
||||||
@Param("principal") String principal, @Param("type") String type, Pageable pageable);
|
@Param("principal") String principal, @Param("type") String type, Pageable pageable);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||||
|
+ " :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||||
Page<PersistentAuditEvent> findByPrincipalAndTimestampBetween(
|
Page<PersistentAuditEvent> findByPrincipalAndTimestampBetween(
|
||||||
@Param("principal") String principal,
|
@Param("principal") String principal,
|
||||||
@Param("startDate") Instant startDate,
|
@Param("startDate") Instant startDate,
|
||||||
@ -45,7 +49,9 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
|||||||
String type, Instant startDate, Instant endDate, Pageable pageable);
|
String type, Instant startDate, Instant endDate, Pageable pageable);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||||
|
+ " :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND"
|
||||||
|
+ " :endDate")
|
||||||
Page<PersistentAuditEvent> findByPrincipalAndTypeAndTimestampBetween(
|
Page<PersistentAuditEvent> findByPrincipalAndTypeAndTimestampBetween(
|
||||||
@Param("principal") String principal,
|
@Param("principal") String principal,
|
||||||
@Param("type") String type,
|
@Param("type") String type,
|
||||||
@ -55,7 +61,8 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
|||||||
|
|
||||||
// Non-paged versions for export
|
// Non-paged versions for export
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||||
|
+ " :principal, '%'))")
|
||||||
List<PersistentAuditEvent> findAllByPrincipalForExport(@Param("principal") String principal);
|
List<PersistentAuditEvent> findAllByPrincipalForExport(@Param("principal") String principal);
|
||||||
|
|
||||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type")
|
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type")
|
||||||
@ -69,26 +76,31 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
|||||||
List<PersistentAuditEvent> findByTimestampAfter(@Param("startDate") Instant startDate);
|
List<PersistentAuditEvent> findByTimestampAfter(@Param("startDate") Instant startDate);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||||
|
+ " :principal, '%')) AND e.type = :type")
|
||||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeForExport(
|
List<PersistentAuditEvent> findAllByPrincipalAndTypeForExport(
|
||||||
@Param("principal") String principal, @Param("type") String type);
|
@Param("principal") String principal, @Param("type") String type);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||||
|
+ " :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||||
List<PersistentAuditEvent> findAllByPrincipalAndTimestampBetweenForExport(
|
List<PersistentAuditEvent> findAllByPrincipalAndTimestampBetweenForExport(
|
||||||
@Param("principal") String principal,
|
@Param("principal") String principal,
|
||||||
@Param("startDate") Instant startDate,
|
@Param("startDate") Instant startDate,
|
||||||
@Param("endDate") Instant endDate);
|
@Param("endDate") Instant endDate);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
"SELECT e FROM PersistentAuditEvent e WHERE e.type = :type AND e.timestamp BETWEEN"
|
||||||
|
+ " :startDate AND :endDate")
|
||||||
List<PersistentAuditEvent> findAllByTypeAndTimestampBetweenForExport(
|
List<PersistentAuditEvent> findAllByTypeAndTimestampBetweenForExport(
|
||||||
@Param("type") String type,
|
@Param("type") String type,
|
||||||
@Param("startDate") Instant startDate,
|
@Param("startDate") Instant startDate,
|
||||||
@Param("endDate") Instant endDate);
|
@Param("endDate") Instant endDate);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%',"
|
||||||
|
+ " :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND"
|
||||||
|
+ " :endDate")
|
||||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
List<PersistentAuditEvent> findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
||||||
@Param("principal") String principal,
|
@Param("principal") String principal,
|
||||||
@Param("type") String type,
|
@Param("type") String type,
|
||||||
@ -112,7 +124,51 @@ public interface PersistentAuditEventRepository extends JpaRepository<Persistent
|
|||||||
@Query("SELECT e.principal, COUNT(e) FROM PersistentAuditEvent e GROUP BY e.principal")
|
@Query("SELECT e.principal, COUNT(e) FROM PersistentAuditEvent e GROUP BY e.principal")
|
||||||
List<Object[]> countByPrincipal();
|
List<Object[]> countByPrincipal();
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT e.type, COUNT(e) FROM PersistentAuditEvent e WHERE e.timestamp BETWEEN"
|
||||||
|
+ " :startDate AND :endDate GROUP BY e.type")
|
||||||
|
List<Object[]> countByTypeBetween(
|
||||||
|
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT e.principal, COUNT(e) FROM PersistentAuditEvent e WHERE e.timestamp BETWEEN"
|
||||||
|
+ " :startDate AND :endDate GROUP BY e.principal")
|
||||||
|
List<Object[]> countByPrincipalBetween(
|
||||||
|
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||||
|
|
||||||
|
// Portable time-bucketing using YEAR/MONTH/DAY functions (works across most dialects)
|
||||||
|
@Query(
|
||||||
|
"SELECT YEAR(e.timestamp), MONTH(e.timestamp), DAY(e.timestamp), COUNT(e) "
|
||||||
|
+ "FROM PersistentAuditEvent e "
|
||||||
|
+ "WHERE e.timestamp BETWEEN :startDate AND :endDate "
|
||||||
|
+ "GROUP BY YEAR(e.timestamp), MONTH(e.timestamp), DAY(e.timestamp) "
|
||||||
|
+ "ORDER BY YEAR(e.timestamp), MONTH(e.timestamp), DAY(e.timestamp)")
|
||||||
|
List<Object[]> histogramByDayBetween(
|
||||||
|
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT HOUR(e.timestamp), COUNT(e) FROM PersistentAuditEvent e WHERE e.timestamp"
|
||||||
|
+ " BETWEEN :startDate AND :endDate GROUP BY HOUR(e.timestamp) ORDER BY"
|
||||||
|
+ " HOUR(e.timestamp)")
|
||||||
|
List<Object[]> histogramByHourBetween(
|
||||||
|
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||||
|
|
||||||
// Get distinct event types for filtering
|
// Get distinct event types for filtering
|
||||||
@Query("SELECT DISTINCT e.type FROM PersistentAuditEvent e ORDER BY e.type")
|
@Query("SELECT DISTINCT e.type FROM PersistentAuditEvent e ORDER BY e.type")
|
||||||
List<String> findDistinctEventTypes();
|
List<String> findDistinctEventTypes();
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT e.principal FROM PersistentAuditEvent e ORDER BY e.principal")
|
||||||
|
List<String> findDistinctPrincipals();
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT DISTINCT e.principal FROM PersistentAuditEvent e WHERE e.type = :type ORDER BY"
|
||||||
|
+ " e.principal")
|
||||||
|
List<String> findDistinctPrincipalsByType(@Param("type") String type);
|
||||||
|
|
||||||
|
// Top/Latest helpers & existence checks
|
||||||
|
Optional<PersistentAuditEvent> findTopByOrderByTimestampDesc();
|
||||||
|
|
||||||
|
Optional<PersistentAuditEvent> findTopByPrincipalOrderByTimestampDesc(String principal);
|
||||||
|
|
||||||
|
Optional<PersistentAuditEvent> findTopByTypeOrderByTimestampDesc(String type);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ import org.springframework.stereotype.Component;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
|
||||||
import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
|
import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -86,12 +85,6 @@ public class LicenseKeyChecker {
|
|||||||
return keyOrFilePath;
|
return keyOrFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateLicenseKey(String newKey) throws IOException {
|
|
||||||
applicationProperties.getPremium().setKey(newKey);
|
|
||||||
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
|
|
||||||
checkLicense();
|
|
||||||
}
|
|
||||||
|
|
||||||
public License getPremiumLicenseEnabledResult() {
|
public License getPremiumLicenseEnabledResult() {
|
||||||
return premiumEnabledResult;
|
return premiumEnabledResult;
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.enumeration.Role;
|
import stirling.software.common.model.enumeration.Role;
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.audit.Audited;
|
||||||
import stirling.software.proprietary.model.Team;
|
import stirling.software.proprietary.model.Team;
|
||||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
@ -82,6 +85,7 @@ public class UserController {
|
|||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/change-username")
|
@PostMapping("/change-username")
|
||||||
|
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||||
public RedirectView changeUsername(
|
public RedirectView changeUsername(
|
||||||
Principal principal,
|
Principal principal,
|
||||||
@RequestParam(name = "currentPasswordChangeUsername") String currentPassword,
|
@RequestParam(name = "currentPasswordChangeUsername") String currentPassword,
|
||||||
@ -125,6 +129,7 @@ public class UserController {
|
|||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/change-password-on-login")
|
@PostMapping("/change-password-on-login")
|
||||||
|
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||||
public RedirectView changePasswordOnLogin(
|
public RedirectView changePasswordOnLogin(
|
||||||
Principal principal,
|
Principal principal,
|
||||||
@RequestParam(name = "currentPassword") String currentPassword,
|
@RequestParam(name = "currentPassword") String currentPassword,
|
||||||
@ -153,6 +158,7 @@ public class UserController {
|
|||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/change-password")
|
@PostMapping("/change-password")
|
||||||
|
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||||
public RedirectView changePassword(
|
public RedirectView changePassword(
|
||||||
Principal principal,
|
Principal principal,
|
||||||
@RequestParam(name = "currentPassword") String currentPassword,
|
@RequestParam(name = "currentPassword") String currentPassword,
|
||||||
|
@ -16,11 +16,16 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.audit.Audited;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CustomOAuth2AuthenticationFailureHandler
|
public class CustomOAuth2AuthenticationFailureHandler
|
||||||
extends SimpleUrlAuthenticationFailureHandler {
|
extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Audited(type = AuditEventType.USER_FAILED_LOGIN, level = AuditLevel.BASIC)
|
||||||
public void onAuthenticationFailure(
|
public void onAuthenticationFailure(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
|
@ -24,6 +24,9 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
import stirling.software.common.util.RequestUriUtils;
|
||||||
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.audit.Audited;
|
||||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||||
@ -39,6 +42,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
private final JwtServiceInterface jwtService;
|
private final JwtServiceInterface jwtService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
||||||
public void onAuthenticationSuccess(
|
public void onAuthenticationSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
@ -14,11 +14,16 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.audit.Audited;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Audited(type = AuditEventType.USER_FAILED_LOGIN, level = AuditLevel.BASIC)
|
||||||
public void onAuthenticationFailure(
|
public void onAuthenticationFailure(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
|
@ -23,6 +23,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
import stirling.software.common.util.RequestUriUtils;
|
||||||
|
import stirling.software.proprietary.audit.AuditEventType;
|
||||||
|
import stirling.software.proprietary.audit.AuditLevel;
|
||||||
|
import stirling.software.proprietary.audit.Audited;
|
||||||
import stirling.software.proprietary.security.model.AuthenticationType;
|
import stirling.software.proprietary.security.model.AuthenticationType;
|
||||||
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
import stirling.software.proprietary.security.service.JwtServiceInterface;
|
||||||
import stirling.software.proprietary.security.service.LoginAttemptService;
|
import stirling.software.proprietary.security.service.LoginAttemptService;
|
||||||
@ -39,6 +42,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
private final JwtServiceInterface jwtService;
|
private final JwtServiceInterface jwtService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
|
||||||
public void onAuthenticationSuccess(
|
public void onAuthenticationSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
@ -218,7 +218,7 @@ function loadAuditData(targetPage, realPageSize) {
|
|||||||
showLoading('table-loading');
|
showLoading('table-loading');
|
||||||
|
|
||||||
// Always request page 0 from server, but with increased page size if needed
|
// Always request page 0 from server, but with increased page size if needed
|
||||||
let url = `/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
let url = `/api/v1/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
||||||
|
|
||||||
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
|
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
|
||||||
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
|
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
|
||||||
@ -302,7 +302,7 @@ function loadStats(days) {
|
|||||||
showLoading('user-chart-loading');
|
showLoading('user-chart-loading');
|
||||||
showLoading('time-chart-loading');
|
showLoading('time-chart-loading');
|
||||||
|
|
||||||
fetchWithCsrf(`/audit/stats?days=${days}`)
|
fetchWithCsrf(`/api/v1/audit/stats?days=${days}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById('total-events').textContent = data.totalEvents;
|
document.getElementById('total-events').textContent = data.totalEvents;
|
||||||
@ -328,7 +328,7 @@ function exportAuditData(format) {
|
|||||||
const startDate = exportStartDateFilter.value;
|
const startDate = exportStartDateFilter.value;
|
||||||
const endDate = exportEndDateFilter.value;
|
const endDate = exportEndDateFilter.value;
|
||||||
|
|
||||||
let url = format === 'json' ? '/audit/export/json?' : '/audit/export?';
|
let url = format === 'json' ? '/api/v1/audit/export/json?' : '/api/v1/audit/export/csv?';
|
||||||
|
|
||||||
if (type) url += `&type=${encodeURIComponent(type)}`;
|
if (type) url += `&type=${encodeURIComponent(type)}`;
|
||||||
if (principal) url += `&principal=${encodeURIComponent(principal)}`;
|
if (principal) url += `&principal=${encodeURIComponent(principal)}`;
|
||||||
@ -835,7 +835,7 @@ function hideLoading(id) {
|
|||||||
|
|
||||||
// Load event types from the server for filter dropdowns
|
// Load event types from the server for filter dropdowns
|
||||||
function loadEventTypes() {
|
function loadEventTypes() {
|
||||||
fetchWithCsrf('/audit/types')
|
fetchWithCsrf('/api/v1/audit/types')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(types => {
|
.then(types => {
|
||||||
if (!types || types.length === 0) {
|
if (!types || types.length === 0) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user