mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
extra stuff
This commit is contained in:
parent
a5aed57d9f
commit
561003f9af
@ -8,13 +8,16 @@ import org.aspectj.lang.annotation.Around;
|
|||||||
import org.aspectj.lang.annotation.Aspect;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||||
import stirling.software.proprietary.service.AuditService;
|
import stirling.software.proprietary.service.AuditService;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aspect for processing {@link Audited} annotations.
|
* Aspect for processing {@link Audited} annotations.
|
||||||
@ -34,14 +37,23 @@ public class AuditAspect {
|
|||||||
Method method = signature.getMethod();
|
Method method = signature.getMethod();
|
||||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||||
|
|
||||||
// Skip if this audit level is not enabled
|
// Use unified check to determine if we should audit
|
||||||
if (!auditConfig.isLevelEnabled(auditedAnnotation.level())) {
|
if (!AuditUtils.shouldAudit(method, auditConfig)) {
|
||||||
return joinPoint.proceed();
|
return joinPoint.proceed();
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> auditData = new HashMap<>();
|
// Use AuditUtils to create the base audit data
|
||||||
auditData.put("className", joinPoint.getTarget().getClass().getName());
|
Map<String, Object> auditData = AuditUtils.createBaseAuditData(joinPoint, auditedAnnotation.level());
|
||||||
auditData.put("methodName", method.getName());
|
|
||||||
|
// Add HTTP information if we're in a web context
|
||||||
|
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attrs != null) {
|
||||||
|
HttpServletRequest req = attrs.getRequest();
|
||||||
|
String path = req.getRequestURI();
|
||||||
|
String httpMethod = req.getMethod();
|
||||||
|
AuditUtils.addHttpData(auditData, httpMethod, path, auditedAnnotation.level());
|
||||||
|
AuditUtils.addFileData(auditData, joinPoint, auditedAnnotation.level());
|
||||||
|
}
|
||||||
|
|
||||||
// Add arguments if requested and if at VERBOSE level, or if specifically requested
|
// Add arguments if requested and if at VERBOSE level, or if specifically requested
|
||||||
boolean includeArgs = auditedAnnotation.includeArgs() &&
|
boolean includeArgs = auditedAnnotation.includeArgs() &&
|
||||||
@ -49,18 +61,11 @@ public class AuditAspect {
|
|||||||
auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
||||||
|
|
||||||
if (includeArgs) {
|
if (includeArgs) {
|
||||||
Object[] args = joinPoint.getArgs();
|
AuditUtils.addMethodArguments(auditData, joinPoint, AuditLevel.VERBOSE);
|
||||||
String[] parameterNames = signature.getParameterNames();
|
|
||||||
|
|
||||||
if (args != null && parameterNames != null) {
|
|
||||||
IntStream.range(0, args.length)
|
|
||||||
.forEach(i -> {
|
|
||||||
String paramName = i < parameterNames.length ? parameterNames[i] : "arg" + i;
|
|
||||||
auditData.put("arg_" + paramName, args[i]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record start time for latency calculation
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
Object result;
|
Object result;
|
||||||
try {
|
try {
|
||||||
// Execute the method
|
// Execute the method
|
||||||
@ -88,17 +93,36 @@ public class AuditAspect {
|
|||||||
// Re-throw the exception
|
// Re-throw the exception
|
||||||
throw ex;
|
throw ex;
|
||||||
} finally {
|
} finally {
|
||||||
// Create the audit entry with the specified level
|
// Add timing information - use isHttpRequest=false to ensure we get timing for non-HTTP methods
|
||||||
// Determine which type of event identifier to use (enum or string)
|
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
||||||
AuditEventType eventType = auditedAnnotation.type();
|
boolean isHttpRequest = attrs != null;
|
||||||
String typeString = auditedAnnotation.typeString();
|
AuditUtils.addTimingData(auditData, startTime, resp, auditedAnnotation.level(), isHttpRequest);
|
||||||
|
|
||||||
if (eventType != AuditEventType.HTTP_REQUEST || !StringUtils.isNotEmpty(typeString)) {
|
// Resolve the event type based on annotation and context
|
||||||
// Use the enum type (preferred)
|
String httpMethod = null;
|
||||||
auditService.audit(eventType, auditData, auditedAnnotation.level());
|
String path = null;
|
||||||
} else {
|
if (attrs != null) {
|
||||||
|
HttpServletRequest req = attrs.getRequest();
|
||||||
|
httpMethod = req.getMethod();
|
||||||
|
path = req.getRequestURI();
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditEventType eventType = AuditUtils.resolveEventType(
|
||||||
|
method,
|
||||||
|
joinPoint.getTarget().getClass(),
|
||||||
|
path,
|
||||||
|
httpMethod,
|
||||||
|
auditedAnnotation
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if we should use string type instead
|
||||||
|
String typeString = auditedAnnotation.typeString();
|
||||||
|
if (eventType == AuditEventType.HTTP_REQUEST && StringUtils.isNotEmpty(typeString)) {
|
||||||
// Use the string type (for backward compatibility)
|
// Use the string type (for backward compatibility)
|
||||||
auditService.audit(typeString, auditData, auditedAnnotation.level());
|
auditService.audit(typeString, auditData, auditedAnnotation.level());
|
||||||
|
} else {
|
||||||
|
// Use the enum type (preferred)
|
||||||
|
auditService.audit(eventType, auditData, auditedAnnotation.level());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,318 @@
|
|||||||
|
package stirling.software.proprietary.audit;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.slf4j.MDC;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import stirling.software.common.util.RequestUriUtils;
|
||||||
|
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared utilities for audit aspects to ensure consistent behavior
|
||||||
|
* across different audit mechanisms.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class AuditUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a standard audit data map with common attributes based on the current audit level
|
||||||
|
*
|
||||||
|
* @param joinPoint The AspectJ join point
|
||||||
|
* @param auditLevel The current audit level
|
||||||
|
* @return A map with standard audit data
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> createBaseAuditData(ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
|
||||||
|
// Common data for all levels
|
||||||
|
data.put("timestamp", Instant.now().toString());
|
||||||
|
|
||||||
|
// Add principal if available
|
||||||
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (auth != null && auth.getName() != null) {
|
||||||
|
data.put("principal", auth.getName());
|
||||||
|
} else {
|
||||||
|
data.put("principal", "system");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add class name and method name only at VERBOSE level
|
||||||
|
if (auditLevel.includes(AuditLevel.VERBOSE)) {
|
||||||
|
data.put("className", joinPoint.getTarget().getClass().getName());
|
||||||
|
data.put("methodName", ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add HTTP-specific information to the audit data if available
|
||||||
|
*
|
||||||
|
* @param data The existing audit data map
|
||||||
|
* @param httpMethod The HTTP method (GET, POST, etc.)
|
||||||
|
* @param path The request path
|
||||||
|
* @param auditLevel The current audit level
|
||||||
|
*/
|
||||||
|
public static void addHttpData(Map<String, Object> data, String httpMethod, String path, AuditLevel auditLevel) {
|
||||||
|
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attrs == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServletRequest req = attrs.getRequest();
|
||||||
|
HttpServletResponse resp = attrs.getResponse();
|
||||||
|
|
||||||
|
// BASIC level HTTP data
|
||||||
|
data.put("httpMethod", httpMethod);
|
||||||
|
data.put("path", path);
|
||||||
|
|
||||||
|
// STANDARD level HTTP data
|
||||||
|
if (auditLevel.includes(AuditLevel.STANDARD)) {
|
||||||
|
data.put("clientIp", req.getRemoteAddr());
|
||||||
|
data.put("sessionId", req.getSession(false) != null ? req.getSession(false).getId() : null);
|
||||||
|
data.put("requestId", MDC.get("requestId"));
|
||||||
|
|
||||||
|
// Form data for POST/PUT/PATCH
|
||||||
|
if (("POST".equalsIgnoreCase(httpMethod) ||
|
||||||
|
"PUT".equalsIgnoreCase(httpMethod) ||
|
||||||
|
"PATCH".equalsIgnoreCase(httpMethod)) && req.getContentType() != null) {
|
||||||
|
|
||||||
|
String contentType = req.getContentType();
|
||||||
|
if (contentType.contains("application/x-www-form-urlencoded") ||
|
||||||
|
contentType.contains("multipart/form-data")) {
|
||||||
|
|
||||||
|
Map<String, String[]> params = new HashMap<>(req.getParameterMap());
|
||||||
|
// Remove CSRF token from logged parameters
|
||||||
|
params.remove("_csrf");
|
||||||
|
|
||||||
|
if (!params.isEmpty()) {
|
||||||
|
data.put("formParams", params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add file information to the audit data if available
|
||||||
|
*
|
||||||
|
* @param data The existing audit data map
|
||||||
|
* @param joinPoint The AspectJ join point
|
||||||
|
* @param auditLevel The current audit level
|
||||||
|
*/
|
||||||
|
public static void addFileData(Map<String, Object> data, ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
||||||
|
if (auditLevel.includes(AuditLevel.STANDARD)) {
|
||||||
|
List<MultipartFile> files = Arrays.stream(joinPoint.getArgs())
|
||||||
|
.filter(a -> a instanceof MultipartFile)
|
||||||
|
.map(a -> (MultipartFile)a)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
List<Map<String,Object>> fileInfos = files.stream().map(f -> {
|
||||||
|
Map<String,Object> m = new HashMap<>();
|
||||||
|
m.put("name", f.getOriginalFilename());
|
||||||
|
m.put("size", f.getSize());
|
||||||
|
m.put("type", f.getContentType());
|
||||||
|
return m;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
|
||||||
|
data.put("files", fileInfos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add method arguments to the audit data
|
||||||
|
*
|
||||||
|
* @param data The existing audit data map
|
||||||
|
* @param joinPoint The AspectJ join point
|
||||||
|
* @param auditLevel The current audit level
|
||||||
|
*/
|
||||||
|
public static void addMethodArguments(Map<String, Object> data, ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
||||||
|
if (auditLevel.includes(AuditLevel.VERBOSE)) {
|
||||||
|
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
||||||
|
String[] names = sig.getParameterNames();
|
||||||
|
Object[] vals = joinPoint.getArgs();
|
||||||
|
if (names != null && vals != null) {
|
||||||
|
IntStream.range(0, names.length)
|
||||||
|
.forEach(i -> data.put("arg_" + names[i], vals[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a method should be audited based on config and annotation
|
||||||
|
*
|
||||||
|
* @param method The method to check
|
||||||
|
* @param auditConfig The audit configuration
|
||||||
|
* @return true if the method should be audited
|
||||||
|
*/
|
||||||
|
public static boolean shouldAudit(Method method, AuditConfigurationProperties auditConfig) {
|
||||||
|
// First check if audit is globally enabled
|
||||||
|
if (!auditConfig.isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for annotation override
|
||||||
|
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||||
|
if (auditedAnnotation != null) {
|
||||||
|
// Method has @Audited - check if the specific level is enabled
|
||||||
|
return auditConfig.isLevelEnabled(auditedAnnotation.level());
|
||||||
|
}
|
||||||
|
|
||||||
|
// No annotation - use global level for controllers
|
||||||
|
return auditConfig.isLevelEnabled(AuditLevel.BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add timing and response status data to the audit record
|
||||||
|
*
|
||||||
|
* @param data The audit data to add to
|
||||||
|
* @param startTime The start time in milliseconds
|
||||||
|
* @param response The HTTP response (may be null for non-HTTP methods)
|
||||||
|
* @param level The current audit level
|
||||||
|
* @param isHttpRequest Whether this is an HTTP request (controller) or a regular method call
|
||||||
|
*/
|
||||||
|
public static void addTimingData(Map<String, Object> data, long startTime, HttpServletResponse response, AuditLevel level, boolean isHttpRequest) {
|
||||||
|
if (level.includes(AuditLevel.STANDARD)) {
|
||||||
|
// For HTTP requests, let ControllerAuditAspect handle timing separately
|
||||||
|
// For non-HTTP methods, add execution time here
|
||||||
|
if (!isHttpRequest) {
|
||||||
|
data.put("latencyMs", System.currentTimeMillis() - startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add HTTP status code if available
|
||||||
|
if (response != null) {
|
||||||
|
data.put("statusCode", response.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the event type to use for auditing, considering annotations and context
|
||||||
|
*
|
||||||
|
* @param method The method being audited
|
||||||
|
* @param controller The controller class
|
||||||
|
* @param path The request path (may be null for non-HTTP methods)
|
||||||
|
* @param httpMethod The HTTP method (may be null for non-HTTP methods)
|
||||||
|
* @param annotation The @Audited annotation (may be null)
|
||||||
|
* @return The resolved event type (never null)
|
||||||
|
*/
|
||||||
|
public static AuditEventType resolveEventType(Method method, Class<?> controller, String path, String httpMethod, Audited annotation) {
|
||||||
|
// First check if we have an explicit annotation
|
||||||
|
if (annotation != null && annotation.type() != AuditEventType.HTTP_REQUEST) {
|
||||||
|
return annotation.type();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For HTTP methods, infer based on controller and path
|
||||||
|
if (httpMethod != null) {
|
||||||
|
String cls = controller.getSimpleName().toLowerCase();
|
||||||
|
String pkg = controller.getPackage().getName().toLowerCase();
|
||||||
|
|
||||||
|
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
|
||||||
|
|
||||||
|
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|
||||||
|
|| path.startsWith("/user") || path.startsWith("/login")) {
|
||||||
|
return AuditEventType.USER_PROFILE_UPDATE;
|
||||||
|
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
|
||||||
|
return AuditEventType.SETTINGS_CHANGED;
|
||||||
|
} else if (cls.contains("file") || path.startsWith("/file")
|
||||||
|
|| path.matches("(?i).*/(upload|download)/.*")) {
|
||||||
|
return AuditEventType.FILE_OPERATION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default for non-HTTP methods or when no specific match
|
||||||
|
return AuditEventType.PDF_PROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the appropriate audit level to use
|
||||||
|
*
|
||||||
|
* @param method The method to check
|
||||||
|
* @param defaultLevel The default level to use if no annotation present
|
||||||
|
* @param auditConfig The audit configuration
|
||||||
|
* @return The audit level to use
|
||||||
|
*/
|
||||||
|
public static AuditLevel getEffectiveAuditLevel(Method method, AuditLevel defaultLevel, AuditConfigurationProperties auditConfig) {
|
||||||
|
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||||
|
if (auditedAnnotation != null) {
|
||||||
|
// Method has @Audited - use its level
|
||||||
|
return auditedAnnotation.level();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use default level (typically from global config)
|
||||||
|
return defaultLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the appropriate audit event type to use
|
||||||
|
*
|
||||||
|
* @param method The method being audited
|
||||||
|
* @param controller The controller class
|
||||||
|
* @param path The request path
|
||||||
|
* @param httpMethod The HTTP method
|
||||||
|
* @return The determined audit event type
|
||||||
|
*/
|
||||||
|
public static AuditEventType determineAuditEventType(Method method, Class<?> controller, String path, String httpMethod) {
|
||||||
|
// First check for explicit annotation
|
||||||
|
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||||
|
if (auditedAnnotation != null) {
|
||||||
|
return auditedAnnotation.type();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise infer from controller and path
|
||||||
|
String cls = controller.getSimpleName().toLowerCase();
|
||||||
|
String pkg = controller.getPackage().getName().toLowerCase();
|
||||||
|
|
||||||
|
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
|
||||||
|
|
||||||
|
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|
||||||
|
|| path.startsWith("/user") || path.startsWith("/login")) {
|
||||||
|
return AuditEventType.USER_PROFILE_UPDATE;
|
||||||
|
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
|
||||||
|
return AuditEventType.SETTINGS_CHANGED;
|
||||||
|
} else if (cls.contains("file") || path.startsWith("/file")
|
||||||
|
|| path.matches("(?i).*/(upload|download)/.*")) {
|
||||||
|
return AuditEventType.FILE_OPERATION;
|
||||||
|
} else {
|
||||||
|
return AuditEventType.PDF_PROCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current HTTP request if available
|
||||||
|
*
|
||||||
|
* @return The current request or null if not in a request context
|
||||||
|
*/
|
||||||
|
public static HttpServletRequest getCurrentRequest() {
|
||||||
|
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
return attrs != null ? attrs.getRequest() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a GET request is for a static resource
|
||||||
|
*
|
||||||
|
* @param request The HTTP request
|
||||||
|
* @return true if this is a static resource request
|
||||||
|
*/
|
||||||
|
public static boolean isStaticResourceRequest(HttpServletRequest request) {
|
||||||
|
return request != null && !RequestUriUtils.isTrackableResource(
|
||||||
|
request.getContextPath(), request.getRequestURI());
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,11 @@ package stirling.software.proprietary.audit;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
import org.slf4j.MDC;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
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;
|
||||||
@ -17,23 +16,16 @@ import org.springframework.web.bind.annotation.PutMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.common.util.RequestUriUtils;
|
|
||||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||||
import stirling.software.proprietary.service.AuditService;
|
import stirling.software.proprietary.service.AuditService;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aspect for automatically auditing controller methods with web mappings
|
* Aspect for automatically auditing controller methods with web mappings
|
||||||
@ -50,11 +42,8 @@ public class ControllerAuditAspect {
|
|||||||
|
|
||||||
|
|
||||||
@Around("execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))")
|
@Around("execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))")
|
||||||
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
|
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
|
||||||
log.info("HELLOOOOOOOOOOOOOOOO");
|
return auditController(jp, "GET");
|
||||||
return auditController(jp, "GET");
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Intercept all methods with GetMapping annotation
|
* Intercept all methods with GetMapping annotation
|
||||||
@ -99,23 +88,28 @@ public class ControllerAuditAspect {
|
|||||||
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
|
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
|
||||||
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
||||||
Method method = sig.getMethod();
|
Method method = sig.getMethod();
|
||||||
AuditLevel level = auditConfig.getAuditLevel();
|
|
||||||
// OFF below BASIC?
|
// Use unified check to determine if we should audit
|
||||||
if (!auditConfig.isLevelEnabled(AuditLevel.BASIC)) {
|
if (!AuditUtils.shouldAudit(method, auditConfig)) {
|
||||||
return joinPoint.proceed();
|
return joinPoint.proceed();
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Opt-out
|
// Check if method is explicitly annotated with @Audited
|
||||||
// if (method.isAnnotationPresent(Audited.class)) {
|
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||||
// return joinPoint.proceed();
|
AuditLevel level = auditConfig.getAuditLevel();
|
||||||
// }
|
|
||||||
|
// If @Audited annotation is present, respect its level setting
|
||||||
|
if (auditedAnnotation != null) {
|
||||||
|
// Use the level from annotation if it's stricter than global level
|
||||||
|
level = auditedAnnotation.level();
|
||||||
|
}
|
||||||
|
|
||||||
String path = getRequestPath(method, httpMethod);
|
String path = getRequestPath(method, httpMethod);
|
||||||
|
|
||||||
// Skip static GET resources
|
// Skip static GET resources
|
||||||
if ("GET".equals(httpMethod)) {
|
if ("GET".equals(httpMethod)) {
|
||||||
HttpServletRequest maybe = getCurrentRequest();
|
HttpServletRequest maybe = AuditUtils.getCurrentRequest();
|
||||||
if (maybe != null && !RequestUriUtils.isTrackableResource(maybe.getContextPath(), maybe.getRequestURI())) {
|
if (maybe != null && AuditUtils.isStaticResourceRequest(maybe)) {
|
||||||
return joinPoint.proceed();
|
return joinPoint.proceed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,64 +119,19 @@ public class ControllerAuditAspect {
|
|||||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
|
|
||||||
// BASIC
|
// Use AuditUtils to create the base audit data
|
||||||
if (level.includes(AuditLevel.BASIC)) {
|
Map<String, Object> data = AuditUtils.createBaseAuditData(joinPoint, level);
|
||||||
data.put("timestamp", Instant.now().toString());
|
|
||||||
data.put("principal", SecurityContextHolder.getContext().getAuthentication().getName());
|
|
||||||
data.put("path", path);
|
|
||||||
data.put("httpMethod", httpMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
// STANDARD
|
// Add HTTP-specific information
|
||||||
if (level.includes(AuditLevel.STANDARD) && req != null) {
|
AuditUtils.addHttpData(data, httpMethod, path, level);
|
||||||
data.put("clientIp", req.getRemoteAddr());
|
|
||||||
data.put("sessionId", req.getSession(false) != null ? req.getSession(false).getId() : null);
|
|
||||||
data.put("requestId", MDC.get("requestId"));
|
|
||||||
|
|
||||||
if ("POST".equalsIgnoreCase(httpMethod)
|
// Add file information if present
|
||||||
|| "PUT".equalsIgnoreCase(httpMethod)
|
AuditUtils.addFileData(data, joinPoint, level);
|
||||||
|| "PATCH".equalsIgnoreCase(httpMethod)) {
|
|
||||||
String ct = req.getContentType();
|
|
||||||
if (ct != null && (
|
|
||||||
ct.contains("application/x-www-form-urlencoded") ||
|
|
||||||
ct.contains("multipart/form-data")
|
|
||||||
)) {
|
|
||||||
Map<String,String[]> params = req.getParameterMap();
|
|
||||||
if (!params.isEmpty()) {
|
|
||||||
data.put("formParams", params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<MultipartFile> files = Arrays.stream(joinPoint.getArgs())
|
// Add method arguments if at VERBOSE level
|
||||||
.filter(a -> a instanceof MultipartFile)
|
|
||||||
.map(a -> (MultipartFile)a)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (!files.isEmpty()) {
|
|
||||||
List<Map<String,Object>> fileInfos = files.stream().map(f -> {
|
|
||||||
Map<String,Object> m = new HashMap<>();
|
|
||||||
m.put("name", f.getOriginalFilename());
|
|
||||||
m.put("size", f.getSize());
|
|
||||||
m.put("type", f.getContentType());
|
|
||||||
return m;
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
|
|
||||||
data.put("files", fileInfos);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// VERBOSE args
|
|
||||||
if (level.includes(AuditLevel.VERBOSE)) {
|
if (level.includes(AuditLevel.VERBOSE)) {
|
||||||
String[] names = sig.getParameterNames();
|
AuditUtils.addMethodArguments(data, joinPoint, level);
|
||||||
Object[] vals = joinPoint.getArgs();
|
|
||||||
if (names != null && vals != null) {
|
|
||||||
IntStream.range(0, names.length).forEach(i -> data.put("arg_" + names[i], vals[i]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object result = null;
|
Object result = null;
|
||||||
@ -195,37 +144,45 @@ public class ControllerAuditAspect {
|
|||||||
data.put("errorMessage", ex.getMessage());
|
data.put("errorMessage", ex.getMessage());
|
||||||
throw ex;
|
throw ex;
|
||||||
} finally {
|
} finally {
|
||||||
// finalize STANDARD
|
// Handle timing directly for HTTP requests
|
||||||
if (level.includes(AuditLevel.STANDARD)) {
|
if (level.includes(AuditLevel.STANDARD)) {
|
||||||
data.put("latencyMs", System.currentTimeMillis() - start);
|
data.put("latencyMs", System.currentTimeMillis() - start);
|
||||||
if (resp != null) data.put("statusCode", resp.getStatus());
|
if (resp != null) data.put("statusCode", resp.getStatus());
|
||||||
}
|
}
|
||||||
// finalize VERBOSE result
|
|
||||||
|
// Call AuditUtils but with isHttpRequest=true to skip additional timing
|
||||||
|
AuditUtils.addTimingData(data, start, resp, level, true);
|
||||||
|
|
||||||
|
// Add result for VERBOSE level
|
||||||
if (level.includes(AuditLevel.VERBOSE) && result != null) {
|
if (level.includes(AuditLevel.VERBOSE) && result != null) {
|
||||||
data.put("result", result.toString());
|
data.put("result", result.toString());
|
||||||
}
|
}
|
||||||
AuditEventType type = determineAuditEventType(joinPoint.getTarget().getClass(), path, httpMethod);
|
|
||||||
auditService.audit(type, data, level);
|
// Resolve the event type using the unified method
|
||||||
|
AuditEventType eventType = AuditUtils.resolveEventType(
|
||||||
|
method,
|
||||||
|
joinPoint.getTarget().getClass(),
|
||||||
|
path,
|
||||||
|
httpMethod,
|
||||||
|
auditedAnnotation
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if we should use string type instead (for backward compatibility)
|
||||||
|
if (auditedAnnotation != null) {
|
||||||
|
String typeString = auditedAnnotation.typeString();
|
||||||
|
if (eventType == AuditEventType.HTTP_REQUEST && StringUtils.isNotEmpty(typeString)) {
|
||||||
|
auditService.audit(typeString, data, level);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the enum type
|
||||||
|
auditService.audit(eventType, data, level);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuditEventType determineAuditEventType(Class<?> controller, String path, String httpMethod) {
|
// Using AuditUtils.determineAuditEventType instead
|
||||||
String cls = controller.getSimpleName().toLowerCase();
|
|
||||||
String pkg = controller.getPackage().getName().toLowerCase();
|
|
||||||
if ("GET".equals(httpMethod)) return AuditEventType.HTTP_REQUEST;
|
|
||||||
if (cls.contains("user") || cls.contains("auth") || pkg.contains("auth")
|
|
||||||
|| path.startsWith("/user") || path.startsWith("/login")) {
|
|
||||||
return AuditEventType.USER_PROFILE_UPDATE;
|
|
||||||
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
|
|
||||||
return AuditEventType.SETTINGS_CHANGED;
|
|
||||||
} else if (cls.contains("file") || path.startsWith("/file")
|
|
||||||
|| path.matches("(?i).*/(upload|download)/.*")) {
|
|
||||||
return AuditEventType.FILE_OPERATION;
|
|
||||||
} else {
|
|
||||||
return AuditEventType.PDF_PROCESS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getRequestPath(Method method, String httpMethod) {
|
private String getRequestPath(Method method, String httpMethod) {
|
||||||
String base = "";
|
String base = "";
|
||||||
@ -248,8 +205,5 @@ public class ControllerAuditAspect {
|
|||||||
return base + mp;
|
return base + mp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpServletRequest getCurrentRequest() {
|
// Using AuditUtils.getCurrentRequest instead
|
||||||
ServletRequestAttributes a = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
||||||
return a != null ? a.getRequest() : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ package stirling.software.proprietary.config;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@ -18,25 +16,22 @@ import stirling.software.proprietary.audit.AuditLevel;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
@Component
|
@Component
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE+ 10)
|
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
|
||||||
public class AuditConfigurationProperties {
|
public class AuditConfigurationProperties {
|
||||||
|
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
private final int level;
|
private final int level;
|
||||||
private final int retentionDays;
|
private final int retentionDays;
|
||||||
private final String licenseType;
|
|
||||||
|
|
||||||
|
public AuditConfigurationProperties(ApplicationProperties applicationProperties) {
|
||||||
public AuditConfigurationProperties(ApplicationProperties applicationProperties, @Qualifier("license") String licenseType) {
|
|
||||||
ApplicationProperties.Premium.ProFeatures.Audit auditConfig =
|
ApplicationProperties.Premium.ProFeatures.Audit auditConfig =
|
||||||
applicationProperties.getPremium().getProFeatures().getAudit();
|
applicationProperties.getPremium().getProFeatures().getAudit();
|
||||||
|
// Read values directly from configuration
|
||||||
this.enabled = auditConfig.isEnabled();
|
this.enabled = auditConfig.isEnabled();
|
||||||
this.level = auditConfig.getLevel();
|
this.level = auditConfig.getLevel();
|
||||||
this.retentionDays = auditConfig.getRetentionDays();
|
this.retentionDays = auditConfig.getRetentionDays();
|
||||||
this.licenseType = licenseType;
|
|
||||||
|
|
||||||
log.info("Initialized audit configuration: enabled={}, level={}, retentionDays={}",
|
log.debug("Initialized audit configuration: enabled={}, level={}, retentionDays={}",
|
||||||
this.enabled, this.level, this.retentionDays);
|
this.enabled, this.level, this.retentionDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,8 +81,8 @@ public class AuditDashboardController {
|
|||||||
@GetMapping("/data")
|
@GetMapping("/data")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public Map<String, Object> getAuditData(
|
public Map<String, Object> getAuditData(
|
||||||
@RequestParam(value = "page", defaultValue = "0") Long page,
|
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||||
@RequestParam(value = "size", defaultValue = "30") Long size,
|
@RequestParam(value = "size", defaultValue = "30") int size,
|
||||||
@RequestParam(value = "type", required = false) String type,
|
@RequestParam(value = "type", required = false) String type,
|
||||||
@RequestParam(value = "principal", required = false) String principal,
|
@RequestParam(value = "principal", required = false) String principal,
|
||||||
@RequestParam(value = "startDate", required = false)
|
@RequestParam(value = "startDate", required = false)
|
||||||
@ -90,12 +90,11 @@ public class AuditDashboardController {
|
|||||||
@RequestParam(value = "endDate", required = false)
|
@RequestParam(value = "endDate", required = false)
|
||||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, HttpServletRequest request) {
|
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, HttpServletRequest request) {
|
||||||
|
|
||||||
log.info("Raw query string: {}", request.getQueryString());
|
|
||||||
|
|
||||||
Pageable pageable = PageRequest.of(page.intValue(), size.intValue(), Sort.by("timestamp").descending());
|
Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").descending());
|
||||||
Page<PersistentAuditEvent> events;
|
Page<PersistentAuditEvent> events;
|
||||||
|
|
||||||
String mode = "unknown";
|
String mode;
|
||||||
|
|
||||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
if (type != null && principal != null && startDate != null && endDate != null) {
|
||||||
mode = "principal + type + startDate + endDate";
|
mode = "principal + type + startDate + endDate";
|
||||||
@ -133,13 +132,6 @@ public class AuditDashboardController {
|
|||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
List<PersistentAuditEvent> content = events.getContent();
|
List<PersistentAuditEvent> content = events.getContent();
|
||||||
Long firstId = content.isEmpty() ? null : content.get(0).getId();
|
|
||||||
Long lastId = content.isEmpty() ? null : content.get(content.size() - 1).getId();
|
|
||||||
|
|
||||||
log.info("Audit request: page={} size={} mode='{}' → result page={} totalElements={} totalPages={} contentSize={}",
|
|
||||||
page, size, mode, events.getNumber(), events.getTotalElements(), events.getTotalPages(), content.size());
|
|
||||||
|
|
||||||
log.info("Audit content ID range: firstId={} lastId={} (descending timestamp)", firstId, lastId);
|
|
||||||
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("content", content);
|
response.put("content", content);
|
||||||
|
@ -6,7 +6,14 @@ import java.util.Map;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
/** Redacts any map values whose keys match common secret/token patterns. */
|
/** Redacts any map values whose keys match common secret/token patterns. */
|
||||||
|
@Slf4j
|
||||||
public final class SecretMasker {
|
public final class SecretMasker {
|
||||||
|
|
||||||
private static final Pattern SENSITIVE =
|
private static final Pattern SENSITIVE =
|
||||||
@ -14,21 +21,34 @@ public final class SecretMasker {
|
|||||||
|
|
||||||
private SecretMasker() {}
|
private SecretMasker() {}
|
||||||
|
|
||||||
public static Object deepMask(Object value) {
|
public static Map<String,Object> mask(Map<String,Object> in) {
|
||||||
|
if (in == null) return null;
|
||||||
|
|
||||||
|
return in.entrySet().stream()
|
||||||
|
.filter(e -> e.getValue() != null)
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
Map.Entry::getKey,
|
||||||
|
e -> deepMaskValue(e.getKey(), e.getValue())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object deepMask(Object value) {
|
||||||
if (value instanceof Map<?,?> m) {
|
if (value instanceof Map<?,?> m) {
|
||||||
return m.entrySet().stream().collect(Collectors.toMap(
|
return m.entrySet().stream()
|
||||||
Map.Entry::getKey,
|
.filter(e -> e.getValue() != null)
|
||||||
e -> deepMaskValue((String)e.getKey(), e.getValue())
|
.collect(Collectors.toMap(
|
||||||
));
|
Map.Entry::getKey,
|
||||||
|
e -> deepMaskValue((String)e.getKey(), e.getValue())
|
||||||
|
));
|
||||||
} else if (value instanceof List<?> list) {
|
} else if (value instanceof List<?> list) {
|
||||||
return list.stream()
|
return list.stream()
|
||||||
.map(SecretMasker::deepMask)
|
.map(SecretMasker::deepMask).toList();
|
||||||
.collect(Collectors.toList());
|
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Object deepMaskValue(String key, Object value) {
|
private static Object deepMaskValue(String key, Object value) {
|
||||||
if (key != null && SENSITIVE.matcher(key).find()) {
|
if (key != null && SENSITIVE.matcher(key).find()) {
|
||||||
return "***REDACTED***";
|
return "***REDACTED***";
|
||||||
@ -36,12 +56,6 @@ public final class SecretMasker {
|
|||||||
return deepMask(value);
|
return deepMask(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String,Object> mask(Map<String,Object> in) {
|
|
||||||
if (in == null) return null;
|
|
||||||
return in.entrySet().stream()
|
|
||||||
.collect(Collectors.toMap(
|
|
||||||
Map.Entry::getKey,
|
|
||||||
e -> deepMaskValue(e.getKey(), e.getValue())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user