change to enterprise + LIKE

This commit is contained in:
Anthony Stirling 2025-06-18 00:21:03 +01:00
parent 75cd0ee407
commit 5dc1d0f950
10 changed files with 124 additions and 60 deletions

View File

@ -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;

View File

@ -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();

View File

@ -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);

View File

@ -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<String> getAuditTypes() {
// Get distinct event types from the database
List<Object[]> results = auditRepository.findDistinctEventTypes();
List<String> dbTypes = results.stream()
.map(row -> (String) row[0])
.collect(Collectors.toList());
List<String> dbTypes = auditRepository.findDistinctEventTypes();
// Include standard enum types in case they're not in the database yet
List<String> 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();
}

View File

@ -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<PersistentAuditEvent, Long> {
// Basic queries
Page<PersistentAuditEvent> findByPrincipal(String principal, Pageable pageable);
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
Page<PersistentAuditEvent> findByPrincipal(@Param("principal") String principal, Pageable pageable);
Page<PersistentAuditEvent> findByType(String type, Pageable pageable);
Page<PersistentAuditEvent> findByTimestampBetween(Instant startDate, Instant endDate, Pageable pageable);
Page<PersistentAuditEvent> findByPrincipalAndType(String principal, String type, Pageable pageable);
Page<PersistentAuditEvent> 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<PersistentAuditEvent> 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<PersistentAuditEvent> findByPrincipalAndTimestampBetween(@Param("principal") String principal, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate, Pageable pageable);
Page<PersistentAuditEvent> findByTypeAndTimestampBetween(String type, Instant startDate, Instant endDate, Pageable pageable);
Page<PersistentAuditEvent> 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<PersistentAuditEvent> 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<PersistentAuditEvent> findByPrincipal(String principal);
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
List<PersistentAuditEvent> findAllByPrincipalForExport(@Param("principal") String principal);
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type")
List<PersistentAuditEvent> findByTypeForExport(@Param("type") String type);
List<PersistentAuditEvent> findByTimestampBetween(Instant startDate, Instant endDate);
List<PersistentAuditEvent> findByTimestampAfter(Instant startDate);
List<PersistentAuditEvent> findByPrincipalAndType(String principal, String type);
List<PersistentAuditEvent> findByPrincipalAndTimestampBetween(String principal, Instant startDate, Instant endDate);
List<PersistentAuditEvent> findByTypeAndTimestampBetween(String type, Instant startDate, Instant endDate);
List<PersistentAuditEvent> findByPrincipalAndTypeAndTimestampBetween(String principal, String type, Instant startDate, Instant endDate);
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.timestamp BETWEEN :startDate AND :endDate")
List<PersistentAuditEvent> findAllByTimestampBetweenForExport(@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.timestamp > :startDate")
List<PersistentAuditEvent> findByTimestampAfter(@Param("startDate") Instant startDate);
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
List<PersistentAuditEvent> 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<PersistentAuditEvent> 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<PersistentAuditEvent> 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<PersistentAuditEvent> 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<Object[]> findDistinctEventTypes();
List<String> findDistinctEventTypes();
}

View File

@ -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 {}

View File

@ -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();
}
}

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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;
}

View File

@ -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

View File

@ -113,7 +113,7 @@
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
</a>
<a href="/audit" th:if="${@runningProOrHigher}" class="data-btn data-btn-secondary" title="Audit Dashboard">
<a href="/audit" th:if="${@runningEE}" class="data-btn data-btn-secondary" title="Audit Dashboard">
<span class="material-symbols-rounded">security</span>
<span>Audit Dashboard</span>
</a>