diff --git a/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index b63acdf26..27cc9b3ca 100644 --- a/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -442,14 +442,6 @@ public class ApplicationProperties { private boolean database; private CustomMetadata customMetadata = new CustomMetadata(); private GoogleDrive googleDrive = new GoogleDrive(); - private Audit audit = new Audit(); - - @Data - public static class Audit { - private boolean enabled = true; - private int level = 2; // 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE - private int retentionDays = 90; - } @Data public static class CustomMetadata { @@ -493,7 +485,15 @@ public class ApplicationProperties { @Data public static class EnterpriseFeatures { private PersistentMetrics persistentMetrics = new PersistentMetrics(); - + private Audit audit = new Audit(); + + @Data + public static class Audit { + private boolean enabled = true; + private int level = 2; // 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE + private int retentionDays = 90; + } + @Data public static class PersistentMetrics { private boolean enabled; diff --git a/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java b/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java index 6ff420ffe..6e30fa4c8 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java +++ b/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java @@ -11,7 +11,7 @@ import stirling.software.proprietary.audit.AuditLevel; /** * Configuration properties for the audit system. - * Reads values from the ApplicationProperties under premium.proFeatures.audit + * Reads values from the ApplicationProperties under premium.enterpriseFeatures.audit */ @Slf4j @Getter @@ -24,8 +24,8 @@ public class AuditConfigurationProperties { private final int retentionDays; public AuditConfigurationProperties(ApplicationProperties applicationProperties) { - ApplicationProperties.Premium.ProFeatures.Audit auditConfig = - applicationProperties.getPremium().getProFeatures().getAudit(); + ApplicationProperties.Premium.EnterpriseFeatures.Audit auditConfig = + applicationProperties.getPremium().getEnterpriseFeatures().getAudit(); // Read values directly from configuration this.enabled = auditConfig.isEnabled(); diff --git a/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java b/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java index 5fa9367f9..bd9a86d89 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java +++ b/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java @@ -51,17 +51,19 @@ public class CustomAuditEventRepository implements AuditEventRepository { } String rid = MDC.get("requestId"); - log.info("AuditEvent clean data (JSON): {}", - mapper.writeValueAsString(clean)); + if (rid != null) { clean = new java.util.HashMap<>(clean); clean.put("requestId", rid); } + String auditEventData = mapper.writeValueAsString(clean); + log.debug("AuditEvent data (JSON): {}",auditEventData); + PersistentAuditEvent ent = PersistentAuditEvent.builder() .principal(ev.getPrincipal()) .type(ev.getType()) - .data(mapper.writeValueAsString(clean)) + .data(auditEventData) .timestamp(ev.getTimestamp()) .build(); repo.save(ent); diff --git a/proprietary/src/main/java/stirling/software/proprietary/controller/AuditDashboardController.java b/proprietary/src/main/java/stirling/software/proprietary/controller/AuditDashboardController.java index 6616fcd47..c871dbfc0 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/controller/AuditDashboardController.java +++ b/proprietary/src/main/java/stirling/software/proprietary/controller/AuditDashboardController.java @@ -40,6 +40,7 @@ import stirling.software.proprietary.audit.AuditLevel; import stirling.software.proprietary.config.AuditConfigurationProperties; import stirling.software.proprietary.model.security.PersistentAuditEvent; import stirling.software.proprietary.repository.PersistentAuditEventRepository; +import stirling.software.proprietary.security.config.EnterpriseEndpoint; /** * Controller for the audit dashboard. @@ -50,6 +51,7 @@ import stirling.software.proprietary.repository.PersistentAuditEventRepository; @RequestMapping("/audit") @PreAuthorize("hasRole('ADMIN')") @RequiredArgsConstructor +@EnterpriseEndpoint public class AuditDashboardController { private final PersistentAuditEventRepository auditRepository; @@ -186,10 +188,7 @@ public class AuditDashboardController { @ResponseBody public List getAuditTypes() { // Get distinct event types from the database - List results = auditRepository.findDistinctEventTypes(); - List dbTypes = results.stream() - .map(row -> (String) row[0]) - .collect(Collectors.toList()); + List dbTypes = auditRepository.findDistinctEventTypes(); // Include standard enum types in case they're not in the database yet List enumTypes = Arrays.stream(AuditEventType.values()) @@ -222,26 +221,26 @@ public class AuditDashboardController { 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.findByPrincipalAndTypeAndTimestampBetween( + events = auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport( principal, type, start, end); } else if (type != null && principal != null) { - events = auditRepository.findByPrincipalAndType(principal, type); + 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.findByTypeAndTimestampBetween(type, start, end); + 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.findByPrincipalAndTimestampBetween(principal, start, end); + 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.findByTimestampBetween(start, end); + events = auditRepository.findAllByTimestampBetweenForExport(start, end); } else if (type != null) { events = auditRepository.findByTypeForExport(type); } else if (principal != null) { - events = auditRepository.findByPrincipal(principal); + events = auditRepository.findAllByPrincipalForExport(principal); } else { events = auditRepository.findAll(); } @@ -290,26 +289,26 @@ public class AuditDashboardController { 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.findByPrincipalAndTypeAndTimestampBetween( + events = auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport( principal, type, start, end); } else if (type != null && principal != null) { - events = auditRepository.findByPrincipalAndType(principal, type); + 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.findByTypeAndTimestampBetween(type, start, end); + 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.findByPrincipalAndTimestampBetween(principal, start, end); + 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.findByTimestampBetween(start, end); + events = auditRepository.findAllByTimestampBetweenForExport(start, end); } else if (type != null) { events = auditRepository.findByTypeForExport(type); } else if (principal != null) { - events = auditRepository.findByPrincipal(principal); + events = auditRepository.findAllByPrincipalForExport(principal); } else { events = auditRepository.findAll(); } diff --git a/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java b/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java index 7a2b1b4c5..60d1ee3ed 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java +++ b/proprietary/src/main/java/stirling/software/proprietary/repository/PersistentAuditEventRepository.java @@ -6,9 +6,11 @@ import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import stirling.software.proprietary.model.security.PersistentAuditEvent; @@ -17,29 +19,40 @@ public interface PersistentAuditEventRepository extends JpaRepository { // Basic queries - Page findByPrincipal(String principal, Pageable pageable); + @Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))") + Page findByPrincipal(@Param("principal") String principal, Pageable pageable); Page findByType(String type, Pageable pageable); Page findByTimestampBetween(Instant startDate, Instant endDate, Pageable pageable); - Page findByPrincipalAndType(String principal, String type, Pageable pageable); - Page findByPrincipalAndTimestampBetween(String principal, Instant startDate, Instant endDate, Pageable pageable); + @Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type") + Page findByPrincipalAndType(@Param("principal") String principal, @Param("type") String type, Pageable pageable); + @Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate") + Page findByPrincipalAndTimestampBetween(@Param("principal") String principal, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate, Pageable pageable); Page findByTypeAndTimestampBetween(String type, Instant startDate, Instant endDate, Pageable pageable); - Page findByPrincipalAndTypeAndTimestampBetween(String principal, String type, Instant startDate, Instant endDate, Pageable pageable); + @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") + Page findByPrincipalAndTypeAndTimestampBetween(@Param("principal") String principal, @Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate, Pageable pageable); // Non-paged versions for export - List findByPrincipal(String principal); + @Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))") + List findAllByPrincipalForExport(@Param("principal") String principal); @Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type") List findByTypeForExport(@Param("type") String type); - List findByTimestampBetween(Instant startDate, Instant endDate); - List findByTimestampAfter(Instant startDate); - List findByPrincipalAndType(String principal, String type); - List findByPrincipalAndTimestampBetween(String principal, Instant startDate, Instant endDate); - List findByTypeAndTimestampBetween(String type, Instant startDate, Instant endDate); - List findByPrincipalAndTypeAndTimestampBetween(String principal, String type, Instant startDate, Instant endDate); + @Query("SELECT e FROM PersistentAuditEvent e WHERE e.timestamp BETWEEN :startDate AND :endDate") + List findAllByTimestampBetweenForExport(@Param("startDate") Instant startDate, @Param("endDate") Instant endDate); + @Query("SELECT e FROM PersistentAuditEvent e WHERE e.timestamp > :startDate") + List findByTimestampAfter(@Param("startDate") Instant startDate); + @Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type") + List findAllByPrincipalAndTypeForExport(@Param("principal") String principal, @Param("type") String type); + @Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate") + List findAllByPrincipalAndTimestampBetweenForExport(@Param("principal") String principal, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate); + @Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate") + List findAllByTypeAndTimestampBetweenForExport(@Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate); + @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") + List findAllByPrincipalAndTypeAndTimestampBetweenForExport(@Param("principal") String principal, @Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate); // Cleanup queries @Query("DELETE FROM PersistentAuditEvent e WHERE e.timestamp < ?1") - @org.springframework.data.jpa.repository.Modifying - @org.springframework.transaction.annotation.Transactional + @Modifying + @Transactional int deleteByTimestampBefore(Instant cutoffDate); // Find IDs for batch deletion - using JPQL with setMaxResults instead of native query @@ -55,5 +68,5 @@ public interface PersistentAuditEventRepository // Get distinct event types for filtering @Query("SELECT DISTINCT e.type FROM PersistentAuditEvent e ORDER BY e.type") - List findDistinctEventTypes(); + List findDistinctEventTypes(); } \ No newline at end of file diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/config/EnterpriseEndpoint.java b/proprietary/src/main/java/stirling/software/proprietary/security/config/EnterpriseEndpoint.java new file mode 100644 index 000000000..800867017 --- /dev/null +++ b/proprietary/src/main/java/stirling/software/proprietary/security/config/EnterpriseEndpoint.java @@ -0,0 +1,11 @@ +package stirling.software.proprietary.security.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Annotation to mark endpoints that require an Enterprise license. */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnterpriseEndpoint {} \ No newline at end of file diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/config/EnterpriseEndpointAspect.java b/proprietary/src/main/java/stirling/software/proprietary/security/config/EnterpriseEndpointAspect.java new file mode 100644 index 000000000..b0189f2bd --- /dev/null +++ b/proprietary/src/main/java/stirling/software/proprietary/security/config/EnterpriseEndpointAspect.java @@ -0,0 +1,30 @@ +package stirling.software.proprietary.security.config; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; + +@Aspect +@Component +public class EnterpriseEndpointAspect { + + private final boolean runningEE; + + public EnterpriseEndpointAspect(@Qualifier("runningEE") boolean runningEE) { + this.runningEE = runningEE; + } + + @Around( + "@annotation(stirling.software.proprietary.security.config.EnterpriseEndpoint) || @within(stirling.software.proprietary.security.config.EnterpriseEndpoint)") + public Object checkEnterpriseAccess(ProceedingJoinPoint joinPoint) throws Throwable { + if (!runningEE) { + throw new ResponseStatusException( + HttpStatus.FORBIDDEN, "This endpoint requires an Enterprise license"); + } + return joinPoint.proceed(); + } +} \ No newline at end of file diff --git a/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java b/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java index 048a424ea..218969ffa 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java +++ b/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java @@ -1,7 +1,7 @@ package stirling.software.proprietary.service; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.security.core.Authentication; @@ -19,11 +19,19 @@ import java.util.Map; */ @Slf4j @Service -@RequiredArgsConstructor public class AuditService { private final AuditEventRepository repository; private final AuditConfigurationProperties auditConfig; + private final boolean runningEE; + + public AuditService(AuditEventRepository repository, + AuditConfigurationProperties auditConfig, + @org.springframework.beans.factory.annotation.Qualifier("runningEE") boolean runningEE) { + this.repository = repository; + this.auditConfig = auditConfig; + this.runningEE = runningEE; + } /** * Record an audit event for the current authenticated user with a specific audit level @@ -34,8 +42,8 @@ public class AuditService { * @param level The minimum audit level required for this event to be logged */ public void audit(AuditEventType type, Map data, AuditLevel level) { - // Skip auditing if this level is not enabled - check first to avoid further processing - if (!auditConfig.isEnabled() || !auditConfig.getAuditLevel().includes(level)) { + // Skip auditing if this level is not enabled or if not Enterprise edition + if (!auditConfig.isEnabled() || !auditConfig.getAuditLevel().includes(level) || !runningEE) { return; } @@ -65,8 +73,8 @@ public class AuditService { * @param level The minimum audit level required for this event to be logged */ public void audit(String principal, AuditEventType type, Map data, AuditLevel level) { - // Skip auditing if this level is not enabled - if (!auditConfig.isLevelEnabled(level)) { + // Skip auditing if this level is not enabled or if not Enterprise edition + if (!auditConfig.isLevelEnabled(level) || !runningEE) { return; } @@ -95,8 +103,8 @@ public class AuditService { * @param level The minimum audit level required for this event to be logged */ public void audit(String type, Map data, AuditLevel level) { - // Skip auditing if this level is not enabled - if (!auditConfig.isLevelEnabled(level)) { + // Skip auditing if this level is not enabled or if not Enterprise edition + if (!auditConfig.isLevelEnabled(level) || !runningEE) { return; } @@ -126,8 +134,8 @@ public class AuditService { * @param level The minimum audit level required for this event to be logged */ public void audit(String principal, String type, Map data, AuditLevel level) { - // Skip auditing if this level is not enabled - if (!auditConfig.isLevelEnabled(level)) { + // Skip auditing if this level is not enabled or if not Enterprise edition + if (!auditConfig.isLevelEnabled(level) || !runningEE) { return; } diff --git a/stirling-pdf/src/main/resources/settings.yml.template b/stirling-pdf/src/main/resources/settings.yml.template index b92c6bad0..d651eff9f 100644 --- a/stirling-pdf/src/main/resources/settings.yml.template +++ b/stirling-pdf/src/main/resources/settings.yml.template @@ -66,10 +66,6 @@ premium: proFeatures: database: true # Enable database features SSOAutoLogin: false - audit: - enabled: true # Enable audit logging - level: 2 # Audit logging level: 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE - retentionDays: 90 # Number of days to retain audit logs CustomMetadata: autoUpdateMetadata: false author: username @@ -80,6 +76,11 @@ premium: clientId: '' apiKey: '' appId: '' + enterpriseFeatures: + audit: + enabled: true # Enable audit logging + level: 2 # Audit logging level: 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE + retentionDays: 90 # Number of days to retain audit logs mail: enabled: false # set to 'true' to enable sending emails diff --git a/stirling-pdf/src/main/resources/templates/adminSettings.html b/stirling-pdf/src/main/resources/templates/adminSettings.html index 1f97ff562..0d14525c1 100644 --- a/stirling-pdf/src/main/resources/templates/adminSettings.html +++ b/stirling-pdf/src/main/resources/templates/adminSettings.html @@ -113,7 +113,7 @@ Usage Statistics - + security Audit Dashboard