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.reflect.MethodSignature;
|
||||
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.service.AuditService;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* Aspect for processing {@link Audited} annotations.
|
||||
@ -34,14 +37,23 @@ public class AuditAspect {
|
||||
Method method = signature.getMethod();
|
||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||
|
||||
// Skip if this audit level is not enabled
|
||||
if (!auditConfig.isLevelEnabled(auditedAnnotation.level())) {
|
||||
// Use unified check to determine if we should audit
|
||||
if (!AuditUtils.shouldAudit(method, auditConfig)) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
Map<String, Object> auditData = new HashMap<>();
|
||||
auditData.put("className", joinPoint.getTarget().getClass().getName());
|
||||
auditData.put("methodName", method.getName());
|
||||
// Use AuditUtils to create the base audit data
|
||||
Map<String, Object> auditData = AuditUtils.createBaseAuditData(joinPoint, auditedAnnotation.level());
|
||||
|
||||
// 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
|
||||
boolean includeArgs = auditedAnnotation.includeArgs() &&
|
||||
@ -49,18 +61,11 @@ public class AuditAspect {
|
||||
auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
||||
|
||||
if (includeArgs) {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
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]);
|
||||
});
|
||||
}
|
||||
AuditUtils.addMethodArguments(auditData, joinPoint, AuditLevel.VERBOSE);
|
||||
}
|
||||
|
||||
// Record start time for latency calculation
|
||||
long startTime = System.currentTimeMillis();
|
||||
Object result;
|
||||
try {
|
||||
// Execute the method
|
||||
@ -88,17 +93,36 @@ public class AuditAspect {
|
||||
// Re-throw the exception
|
||||
throw ex;
|
||||
} finally {
|
||||
// Create the audit entry with the specified level
|
||||
// Determine which type of event identifier to use (enum or string)
|
||||
AuditEventType eventType = auditedAnnotation.type();
|
||||
String typeString = auditedAnnotation.typeString();
|
||||
// Add timing information - use isHttpRequest=false to ensure we get timing for non-HTTP methods
|
||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
||||
boolean isHttpRequest = attrs != null;
|
||||
AuditUtils.addTimingData(auditData, startTime, resp, auditedAnnotation.level(), isHttpRequest);
|
||||
|
||||
if (eventType != AuditEventType.HTTP_REQUEST || !StringUtils.isNotEmpty(typeString)) {
|
||||
// Use the enum type (preferred)
|
||||
auditService.audit(eventType, auditData, auditedAnnotation.level());
|
||||
} else {
|
||||
// Resolve the event type based on annotation and context
|
||||
String httpMethod = null;
|
||||
String path = null;
|
||||
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)
|
||||
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.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
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.web.bind.annotation.DeleteMapping;
|
||||
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.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.service.AuditService;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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(..))")
|
||||
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
|
||||
log.info("HELLOOOOOOOOOOOOOOOO");
|
||||
return auditController(jp, "GET");
|
||||
|
||||
|
||||
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
|
||||
return auditController(jp, "GET");
|
||||
}
|
||||
/**
|
||||
* Intercept all methods with GetMapping annotation
|
||||
@ -99,23 +88,28 @@ public class ControllerAuditAspect {
|
||||
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
|
||||
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = sig.getMethod();
|
||||
AuditLevel level = auditConfig.getAuditLevel();
|
||||
// OFF below BASIC?
|
||||
if (!auditConfig.isLevelEnabled(AuditLevel.BASIC)) {
|
||||
|
||||
// Use unified check to determine if we should audit
|
||||
if (!AuditUtils.shouldAudit(method, auditConfig)) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
// // Opt-out
|
||||
// if (method.isAnnotationPresent(Audited.class)) {
|
||||
// return joinPoint.proceed();
|
||||
// }
|
||||
|
||||
// Check if method is explicitly annotated with @Audited
|
||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||
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);
|
||||
|
||||
// Skip static GET resources
|
||||
if ("GET".equals(httpMethod)) {
|
||||
HttpServletRequest maybe = getCurrentRequest();
|
||||
if (maybe != null && !RequestUriUtils.isTrackableResource(maybe.getContextPath(), maybe.getRequestURI())) {
|
||||
HttpServletRequest maybe = AuditUtils.getCurrentRequest();
|
||||
if (maybe != null && AuditUtils.isStaticResourceRequest(maybe)) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
}
|
||||
@ -125,64 +119,19 @@ public class ControllerAuditAspect {
|
||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
// BASIC
|
||||
if (level.includes(AuditLevel.BASIC)) {
|
||||
data.put("timestamp", Instant.now().toString());
|
||||
data.put("principal", SecurityContextHolder.getContext().getAuthentication().getName());
|
||||
data.put("path", path);
|
||||
data.put("httpMethod", httpMethod);
|
||||
}
|
||||
|
||||
// STANDARD
|
||||
if (level.includes(AuditLevel.STANDARD) && req != null) {
|
||||
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)
|
||||
|| "PUT".equalsIgnoreCase(httpMethod)
|
||||
|| "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())
|
||||
.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
|
||||
|
||||
// Use AuditUtils to create the base audit data
|
||||
Map<String, Object> data = AuditUtils.createBaseAuditData(joinPoint, level);
|
||||
|
||||
// Add HTTP-specific information
|
||||
AuditUtils.addHttpData(data, httpMethod, path, level);
|
||||
|
||||
// Add file information if present
|
||||
AuditUtils.addFileData(data, joinPoint, level);
|
||||
|
||||
// Add method arguments if at VERBOSE level
|
||||
if (level.includes(AuditLevel.VERBOSE)) {
|
||||
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]));
|
||||
}
|
||||
AuditUtils.addMethodArguments(data, joinPoint, level);
|
||||
}
|
||||
|
||||
Object result = null;
|
||||
@ -195,37 +144,45 @@ public class ControllerAuditAspect {
|
||||
data.put("errorMessage", ex.getMessage());
|
||||
throw ex;
|
||||
} finally {
|
||||
// finalize STANDARD
|
||||
// Handle timing directly for HTTP requests
|
||||
if (level.includes(AuditLevel.STANDARD)) {
|
||||
data.put("latencyMs", System.currentTimeMillis() - start);
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
private AuditEventType determineAuditEventType(Class<?> controller, String path, String httpMethod) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Using AuditUtils.determineAuditEventType instead
|
||||
|
||||
private String getRequestPath(Method method, String httpMethod) {
|
||||
String base = "";
|
||||
@ -248,8 +205,5 @@ public class ControllerAuditAspect {
|
||||
return base + mp;
|
||||
}
|
||||
|
||||
private HttpServletRequest getCurrentRequest() {
|
||||
ServletRequestAttributes a = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return a != null ? a.getRequest() : null;
|
||||
}
|
||||
// Using AuditUtils.getCurrentRequest instead
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ package stirling.software.proprietary.config;
|
||||
import lombok.Getter;
|
||||
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.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -18,25 +16,22 @@ import stirling.software.proprietary.audit.AuditLevel;
|
||||
@Slf4j
|
||||
@Getter
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE+ 10)
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
|
||||
public class AuditConfigurationProperties {
|
||||
|
||||
private final boolean enabled;
|
||||
private final int level;
|
||||
private final int retentionDays;
|
||||
private final String licenseType;
|
||||
|
||||
|
||||
public AuditConfigurationProperties(ApplicationProperties applicationProperties, @Qualifier("license") String licenseType) {
|
||||
public AuditConfigurationProperties(ApplicationProperties applicationProperties) {
|
||||
ApplicationProperties.Premium.ProFeatures.Audit auditConfig =
|
||||
applicationProperties.getPremium().getProFeatures().getAudit();
|
||||
|
||||
applicationProperties.getPremium().getProFeatures().getAudit();
|
||||
// Read values directly from configuration
|
||||
this.enabled = auditConfig.isEnabled();
|
||||
this.level = auditConfig.getLevel();
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -81,8 +81,8 @@ public class AuditDashboardController {
|
||||
@GetMapping("/data")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getAuditData(
|
||||
@RequestParam(value = "page", defaultValue = "0") Long page,
|
||||
@RequestParam(value = "size", defaultValue = "30") Long size,
|
||||
@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)
|
||||
@ -90,12 +90,11 @@ public class AuditDashboardController {
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@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;
|
||||
|
||||
String mode = "unknown";
|
||||
String mode;
|
||||
|
||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
||||
mode = "principal + type + startDate + endDate";
|
||||
@ -133,13 +132,6 @@ public class AuditDashboardController {
|
||||
|
||||
// Logging
|
||||
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<>();
|
||||
response.put("content", content);
|
||||
|
@ -6,7 +6,14 @@ import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
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. */
|
||||
@Slf4j
|
||||
public final class SecretMasker {
|
||||
|
||||
private static final Pattern SENSITIVE =
|
||||
@ -14,21 +21,34 @@ public final class 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) {
|
||||
return m.entrySet().stream().collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
e -> deepMaskValue((String)e.getKey(), e.getValue())
|
||||
));
|
||||
return m.entrySet().stream()
|
||||
.filter(e -> e.getValue() != null)
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
e -> deepMaskValue((String)e.getKey(), e.getValue())
|
||||
));
|
||||
} else if (value instanceof List<?> list) {
|
||||
return list.stream()
|
||||
.map(SecretMasker::deepMask)
|
||||
.collect(Collectors.toList());
|
||||
.map(SecretMasker::deepMask).toList();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Object deepMaskValue(String key, Object value) {
|
||||
if (key != null && SENSITIVE.matcher(key).find()) {
|
||||
return "***REDACTED***";
|
||||
@ -36,12 +56,6 @@ public final class SecretMasker {
|
||||
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