mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 07:55:07 +00:00
Changes to enhance audit for files
This commit is contained in:
parent
cab726ad77
commit
a5aed57d9f
@ -6,6 +6,8 @@ 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;
|
||||
@ -15,16 +17,22 @@ 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 jakarta.servlet.http.HttpServletRequest;
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -40,12 +48,20 @@ public class ControllerAuditAspect {
|
||||
private final AuditService auditService;
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
|
||||
|
||||
@Around("execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))")
|
||||
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
|
||||
log.info("HELLOOOOOOOOOOOOOOOO");
|
||||
return auditController(jp, "GET");
|
||||
|
||||
|
||||
}
|
||||
/**
|
||||
* Intercept all methods with GetMapping annotation
|
||||
*/
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
|
||||
public Object auditGetMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditControllerMethod(joinPoint, "GET");
|
||||
return auditController(joinPoint, "GET");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,7 +69,7 @@ public class ControllerAuditAspect {
|
||||
*/
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
|
||||
public Object auditPostMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditControllerMethod(joinPoint, "POST");
|
||||
return auditController(joinPoint, "POST");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,7 +77,7 @@ public class ControllerAuditAspect {
|
||||
*/
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.PutMapping)")
|
||||
public Object auditPutMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditControllerMethod(joinPoint, "PUT");
|
||||
return auditController(joinPoint, "PUT");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,7 +85,7 @@ public class ControllerAuditAspect {
|
||||
*/
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
|
||||
public Object auditDeleteMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditControllerMethod(joinPoint, "DELETE");
|
||||
return auditController(joinPoint, "DELETE");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,194 +93,163 @@ public class ControllerAuditAspect {
|
||||
*/
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.PatchMapping)")
|
||||
public Object auditPatchMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditControllerMethod(joinPoint, "PATCH");
|
||||
return auditController(joinPoint, "PATCH");
|
||||
}
|
||||
|
||||
/**
|
||||
* Common method to audit controller methods
|
||||
*/
|
||||
private Object auditControllerMethod(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
|
||||
// Skip if below STANDARD level (controller auditing is considered STANDARD level)
|
||||
if (!auditConfig.isLevelEnabled(AuditLevel.STANDARD)) {
|
||||
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)) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
// // Opt-out
|
||||
// if (method.isAnnotationPresent(Audited.class)) {
|
||||
// return joinPoint.proceed();
|
||||
// }
|
||||
|
||||
// Don't audit methods that already have @Audited annotation
|
||||
if (method.isAnnotationPresent(Audited.class)) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
// Get the request path
|
||||
String path = getRequestPath(method, httpMethod);
|
||||
|
||||
// Skip auditing static resources for GET requests
|
||||
// Skip static GET resources
|
||||
if ("GET".equals(httpMethod)) {
|
||||
HttpServletRequest request = getCurrentRequest();
|
||||
if (request != null && RequestUriUtils.isStaticResource(request.getContextPath(), request.getRequestURI())) {
|
||||
HttpServletRequest maybe = getCurrentRequest();
|
||||
if (maybe != null && !RequestUriUtils.isTrackableResource(maybe.getContextPath(), maybe.getRequestURI())) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
}
|
||||
|
||||
// Create audit data
|
||||
Map<String, Object> auditData = new HashMap<>();
|
||||
auditData.put("controller", joinPoint.getTarget().getClass().getSimpleName());
|
||||
auditData.put("method", method.getName());
|
||||
auditData.put("httpMethod", httpMethod);
|
||||
auditData.put("path", path);
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
HttpServletRequest req = attrs != null ? attrs.getRequest() : null;
|
||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
||||
|
||||
// Add method parameters if at VERBOSE level
|
||||
if (auditConfig.isLevelEnabled(AuditLevel.VERBOSE)) {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
String[] parameterNames = signature.getParameterNames();
|
||||
long start = System.currentTimeMillis();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
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]);
|
||||
});
|
||||
// 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
|
||||
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]));
|
||||
}
|
||||
}
|
||||
|
||||
Object result;
|
||||
Object result = null;
|
||||
try {
|
||||
// Execute the method
|
||||
result = joinPoint.proceed();
|
||||
|
||||
// Add success status
|
||||
auditData.put("status", "success");
|
||||
|
||||
// Add result if at VERBOSE level
|
||||
if (auditConfig.isLevelEnabled(AuditLevel.VERBOSE) && result != null) {
|
||||
auditData.put("resultType", result.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
return result;
|
||||
data.put("outcome", "success");
|
||||
} catch (Throwable ex) {
|
||||
// Always add failure information
|
||||
auditData.put("status", "failure");
|
||||
auditData.put("errorType", ex.getClass().getName());
|
||||
auditData.put("errorMessage", ex.getMessage());
|
||||
|
||||
// Re-throw the exception
|
||||
data.put("outcome", "failure");
|
||||
data.put("errorType", ex.getClass().getSimpleName());
|
||||
data.put("errorMessage", ex.getMessage());
|
||||
throw ex;
|
||||
} finally {
|
||||
// Determine the appropriate audit event type based on the controller package and class name
|
||||
AuditEventType eventType = determineAuditEventType(joinPoint.getTarget().getClass(), path, httpMethod);
|
||||
|
||||
// Create the audit entry using the enum type
|
||||
auditService.audit(eventType, auditData, AuditLevel.STANDARD);
|
||||
// finalize STANDARD
|
||||
if (level.includes(AuditLevel.STANDARD)) {
|
||||
data.put("latencyMs", System.currentTimeMillis() - start);
|
||||
if (resp != null) data.put("statusCode", resp.getStatus());
|
||||
}
|
||||
// finalize VERBOSE result
|
||||
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);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the appropriate audit event type based on the controller's package and class name and HTTP method
|
||||
*/
|
||||
private AuditEventType determineAuditEventType(Class<?> controllerClass, String path, String httpMethod) {
|
||||
String className = controllerClass.getSimpleName().toLowerCase();
|
||||
String packageName = controllerClass.getPackage().getName().toLowerCase();
|
||||
|
||||
// For GET requests, just use HTTP_REQUEST as they don't process anything
|
||||
if (httpMethod.equals("GET")) {
|
||||
return AuditEventType.HTTP_REQUEST;
|
||||
}
|
||||
|
||||
// For actual processing operations (POST, PUT, DELETE, etc.)
|
||||
|
||||
// User/authentication related controllers
|
||||
if (className.contains("user") || className.contains("auth") ||
|
||||
packageName.contains("security") || packageName.contains("auth") ||
|
||||
path.startsWith("/user") || path.startsWith("/login") ||
|
||||
path.startsWith("/auth") || path.startsWith("/account")) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Admin related controllers
|
||||
else if (className.contains("admin") || path.startsWith("/admin") ||
|
||||
path.startsWith("/settings") || className.contains("setting") ||
|
||||
className.contains("database") || path.contains("database")) {
|
||||
} else if (cls.contains("admin") || path.startsWith("/admin") || path.startsWith("/settings")) {
|
||||
return AuditEventType.SETTINGS_CHANGED;
|
||||
}
|
||||
|
||||
// File operations - using path prefixes to avoid false matches
|
||||
else if (className.contains("file") ||
|
||||
path.startsWith("/file") ||
|
||||
path.startsWith("/files/") ||
|
||||
path.matches("(?i).*/(upload|download)/.*")) {
|
||||
} else if (cls.contains("file") || path.startsWith("/file")
|
||||
|| path.matches("(?i).*/(upload|download)/.*")) {
|
||||
return AuditEventType.FILE_OPERATION;
|
||||
}
|
||||
|
||||
// Default to PDF operations for most controllers
|
||||
else {
|
||||
} else {
|
||||
return AuditEventType.PDF_PROCESS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the request path from the method's annotations
|
||||
*/
|
||||
private String getRequestPath(Method method, String httpMethod) {
|
||||
// Check class level RequestMapping
|
||||
String basePath = "";
|
||||
RequestMapping classMapping = method.getDeclaringClass().getAnnotation(RequestMapping.class);
|
||||
if (classMapping != null && classMapping.value().length > 0) {
|
||||
basePath = classMapping.value()[0];
|
||||
}
|
||||
|
||||
// Check method level mapping
|
||||
String methodPath = "";
|
||||
Annotation annotation = null;
|
||||
|
||||
switch (httpMethod) {
|
||||
case "GET":
|
||||
annotation = method.getAnnotation(GetMapping.class);
|
||||
if (annotation != null) {
|
||||
String[] paths = ((GetMapping) annotation).value();
|
||||
if (paths.length > 0) methodPath = paths[0];
|
||||
}
|
||||
break;
|
||||
case "POST":
|
||||
annotation = method.getAnnotation(PostMapping.class);
|
||||
if (annotation != null) {
|
||||
String[] paths = ((PostMapping) annotation).value();
|
||||
if (paths.length > 0) methodPath = paths[0];
|
||||
}
|
||||
break;
|
||||
case "PUT":
|
||||
annotation = method.getAnnotation(PutMapping.class);
|
||||
if (annotation != null) {
|
||||
String[] paths = ((PutMapping) annotation).value();
|
||||
if (paths.length > 0) methodPath = paths[0];
|
||||
}
|
||||
break;
|
||||
case "DELETE":
|
||||
annotation = method.getAnnotation(DeleteMapping.class);
|
||||
if (annotation != null) {
|
||||
String[] paths = ((DeleteMapping) annotation).value();
|
||||
if (paths.length > 0) methodPath = paths[0];
|
||||
}
|
||||
break;
|
||||
case "PATCH":
|
||||
annotation = method.getAnnotation(PatchMapping.class);
|
||||
if (annotation != null) {
|
||||
String[] paths = ((PatchMapping) annotation).value();
|
||||
if (paths.length > 0) methodPath = paths[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Combine base path and method path
|
||||
return basePath + methodPath;
|
||||
String base = "";
|
||||
RequestMapping cm = method.getDeclaringClass().getAnnotation(RequestMapping.class);
|
||||
if (cm != null && cm.value().length > 0) base = cm.value()[0];
|
||||
String mp = "";
|
||||
Annotation ann = switch (httpMethod) {
|
||||
case "GET" -> method.getAnnotation(GetMapping.class);
|
||||
case "POST" -> method.getAnnotation(PostMapping.class);
|
||||
case "PUT" -> method.getAnnotation(PutMapping.class);
|
||||
case "DELETE" -> method.getAnnotation(DeleteMapping.class);
|
||||
case "PATCH" -> method.getAnnotation(PatchMapping.class);
|
||||
default -> null;
|
||||
};
|
||||
if (ann instanceof GetMapping gm && gm.value().length > 0) mp = gm.value()[0];
|
||||
if (ann instanceof PostMapping pm && pm.value().length > 0) mp = pm.value()[0];
|
||||
if (ann instanceof PutMapping pum && pum.value().length > 0) mp = pum.value()[0];
|
||||
if (ann instanceof DeleteMapping dm && dm.value().length > 0) mp = dm.value()[0];
|
||||
if (ann instanceof PatchMapping pam && pam.value().length > 0) mp = pam.value()[0];
|
||||
return base + mp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current HttpServletRequest from the RequestContextHolder
|
||||
*/
|
||||
private HttpServletRequest getCurrentRequest() {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return attributes != null ? attributes.getRequest() : null;
|
||||
ServletRequestAttributes a = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return a != null ? a.getRequest() : null;
|
||||
}
|
||||
}
|
@ -1,16 +1,45 @@
|
||||
package stirling.software.proprietary.config;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.TaskDecorator;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
/**
|
||||
* MDC context-propagating task decorator
|
||||
* Copies MDC context from the caller thread to the async executor thread
|
||||
*/
|
||||
static class MDCContextTaskDecorator implements TaskDecorator {
|
||||
@Override
|
||||
public Runnable decorate(Runnable runnable) {
|
||||
// Capture the MDC context from the current thread
|
||||
Map<String, String> contextMap = MDC.getCopyOfContextMap();
|
||||
|
||||
return () -> {
|
||||
try {
|
||||
// Set the captured context on the worker thread
|
||||
if (contextMap != null) {
|
||||
MDC.setContextMap(contextMap);
|
||||
}
|
||||
// Execute the task
|
||||
runnable.run();
|
||||
} finally {
|
||||
// Clear the context to prevent memory leaks
|
||||
MDC.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Bean(name = "auditExecutor")
|
||||
public Executor auditExecutor() {
|
||||
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
|
||||
@ -18,6 +47,10 @@ public class AsyncConfig {
|
||||
exec.setMaxPoolSize(8);
|
||||
exec.setQueueCapacity(1_000);
|
||||
exec.setThreadNamePrefix("audit-");
|
||||
|
||||
// Set the task decorator to propagate MDC context
|
||||
exec.setTaskDecorator(new MDCContextTaskDecorator());
|
||||
|
||||
exec.initialize();
|
||||
return exec;
|
||||
}
|
||||
|
@ -2,7 +2,11 @@ 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;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
@ -14,19 +18,23 @@ import stirling.software.proprietary.audit.AuditLevel;
|
||||
@Slf4j
|
||||
@Getter
|
||||
@Component
|
||||
@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) {
|
||||
|
||||
public AuditConfigurationProperties(ApplicationProperties applicationProperties, @Qualifier("license") String licenseType) {
|
||||
ApplicationProperties.Premium.ProFeatures.Audit auditConfig =
|
||||
applicationProperties.getPremium().getProFeatures().getAudit();
|
||||
|
||||
this.enabled = auditConfig.isEnabled();
|
||||
this.level = auditConfig.getLevel();
|
||||
this.retentionDays = auditConfig.getRetentionDays();
|
||||
this.licenseType = licenseType;
|
||||
|
||||
log.info("Initialized audit configuration: enabled={}, level={}, retentionDays={}",
|
||||
this.enabled, this.level, this.retentionDays);
|
||||
|
@ -2,6 +2,8 @@ package stirling.software.proprietary.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||
@ -20,6 +22,7 @@ import java.util.Map;
|
||||
@Component
|
||||
@Primary
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CustomAuditEventRepository implements AuditEventRepository {
|
||||
|
||||
private final PersistentAuditEventRepository repo;
|
||||
@ -41,7 +44,15 @@ public class CustomAuditEventRepository implements AuditEventRepository {
|
||||
? Map.of()
|
||||
: SecretMasker.mask(ev.getData());
|
||||
|
||||
|
||||
if (clean.isEmpty() ||
|
||||
(clean.size() == 1 && clean.containsKey("details"))) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
|
@ -1,116 +0,0 @@
|
||||
package stirling.software.proprietary.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.support.ServletRequestHandledEvent;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.util.SecretMasker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class HttpRequestAuditPublisher
|
||||
implements ApplicationListener<ServletRequestHandledEvent> {
|
||||
|
||||
private final AuditEventRepository repo;
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ServletRequestHandledEvent e) {
|
||||
// Skip if audit is disabled or level is OFF
|
||||
if (!auditConfig.isEnabled() || auditConfig.getAuditLevel() == AuditLevel.OFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic request information is included at level STANDARD or higher
|
||||
AuditLevel currentLevel = auditConfig.getAuditLevel();
|
||||
boolean isBasicLevel = currentLevel.includes(AuditLevel.BASIC);
|
||||
boolean isStandardLevel = currentLevel.includes(AuditLevel.STANDARD);
|
||||
boolean isVerboseLevel = currentLevel.includes(AuditLevel.VERBOSE);
|
||||
|
||||
// Special case for errors - always log errors at BASIC level
|
||||
boolean isError = e.getStatusCode() >= 400 || e.getFailureCause() != null;
|
||||
|
||||
// Skip non-error requests if below STANDARD level
|
||||
if (!isStandardLevel && !isError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a mutable map to hold all our audit data
|
||||
Map<String, Object> raw = new HashMap<>();
|
||||
|
||||
// Add basic request data from the event (always included)
|
||||
raw.put("method", e.getMethod());
|
||||
raw.put("uri", e.getRequestUrl());
|
||||
raw.put("status", e.getStatusCode());
|
||||
raw.put("latency", e.getProcessingTimeMillis());
|
||||
raw.put("ip", e.getClientAddress());
|
||||
|
||||
// Add standard level data
|
||||
if (isStandardLevel || isError) {
|
||||
raw.put("servlet", e.getServletName());
|
||||
raw.put("sessionId", e.getSessionId());
|
||||
raw.put("requestId", MDC.get("requestId"));
|
||||
raw.put("host", getHostName());
|
||||
raw.put("timestamp", System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// Check for failure information (always included for errors)
|
||||
if (e.getFailureCause() != null) {
|
||||
raw.put("failed", true);
|
||||
raw.put("errorType", e.getFailureCause().getClass().getName());
|
||||
raw.put("errorMessage", e.getFailureCause().getMessage());
|
||||
}
|
||||
|
||||
// Add additional data from MDC at VERBOSE level
|
||||
if (isVerboseLevel) {
|
||||
addFromMDC(raw, "userAgent");
|
||||
addFromMDC(raw, "referer");
|
||||
addFromMDC(raw, "acceptLanguage");
|
||||
addFromMDC(raw, "contentType");
|
||||
addFromMDC(raw, "userRoles");
|
||||
addFromMDC(raw, "queryParams");
|
||||
}
|
||||
|
||||
// Determine the correct audit level for this event
|
||||
AuditLevel eventLevel = isError ? AuditLevel.BASIC :
|
||||
isVerboseLevel ? AuditLevel.VERBOSE :
|
||||
AuditLevel.STANDARD;
|
||||
|
||||
// Create the audit event
|
||||
repo.add(new AuditEvent(
|
||||
e.getUserName() != null ? e.getUserName() : "anonymous",
|
||||
"HTTP_REQUEST",
|
||||
SecretMasker.mask(raw)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a value from MDC to the audit data map if present
|
||||
*/
|
||||
private void addFromMDC(Map<String, Object> data, String key) {
|
||||
String value = MDC.get(key);
|
||||
if (StringUtils.hasText(value)) {
|
||||
data.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the hostname of the current server
|
||||
*/
|
||||
private String getHostName() {
|
||||
try {
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
} catch (UnknownHostException e) {
|
||||
return "unknown-host";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
package stirling.software.proprietary.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.audit.Audited;
|
||||
import stirling.software.proprietary.service.AuditService;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Example controller showing how to use the audit service.
|
||||
* This is for demonstration purposes only and should be removed in production.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/audit-demo")
|
||||
@RequiredArgsConstructor
|
||||
public class AuditExampleController {
|
||||
|
||||
private final AuditService auditService;
|
||||
|
||||
/**
|
||||
* Example using direct AuditService injection
|
||||
*/
|
||||
@GetMapping("/manual/{id}")
|
||||
public String auditManually(@PathVariable String id) {
|
||||
// Create an example audit event manually
|
||||
auditService.audit("EXAMPLE_EVENT", Map.of(
|
||||
"id", id,
|
||||
"timestamp", System.currentTimeMillis(),
|
||||
"action", "view"
|
||||
));
|
||||
|
||||
return "Audit event created for ID: " + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example using @Audited annotation with basic level
|
||||
*/
|
||||
@Audited(type = AuditEventType.USER_PROFILE_UPDATE, level = AuditLevel.BASIC)
|
||||
@PostMapping("/users")
|
||||
public ResponseEntity<Map<String, Object>> createUser(@RequestBody Map<String, Object> user) {
|
||||
// This method is automatically audited with the USER_REGISTRATION type at BASIC level
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("id", "user123");
|
||||
result.put("username", user.get("username"));
|
||||
result.put("created", true);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Example using @Audited annotation with file upload at VERBOSE level
|
||||
*/
|
||||
@Audited(type = AuditEventType.FILE_OPERATION, level = AuditLevel.VERBOSE, includeResult = true)
|
||||
@PostMapping("/files/process")
|
||||
public ResponseEntity<Map<String, Object>> processFile(MultipartFile file) {
|
||||
// This method is automatically audited at VERBOSE level
|
||||
// The audit event will include information about the file
|
||||
// And will also include the result because includeResult=true
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("filename", file != null ? file.getOriginalFilename() : "null");
|
||||
result.put("size", file != null ? file.getSize() : 0);
|
||||
result.put("status", "processed");
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically audited controller method with GetMapping.
|
||||
* This method does NOT have an @Audited annotation but will still be
|
||||
* automatically audited by the ControllerAuditAspect.
|
||||
*/
|
||||
@GetMapping("/users/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getUser(@PathVariable String id) {
|
||||
// This method will be automatically audited by the ControllerAuditAspect
|
||||
// The audit will include the controller name, method name, and path
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("id", id);
|
||||
result.put("username", "johndoe");
|
||||
result.put("email", "john.doe@example.com");
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically audited controller method with PutMapping.
|
||||
*/
|
||||
@PutMapping("/users/{id}")
|
||||
public ResponseEntity<Map<String, Object>> updateUser(
|
||||
@PathVariable String id,
|
||||
@RequestBody Map<String, Object> user) {
|
||||
// This method will be automatically audited by the ControllerAuditAspect
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("id", id);
|
||||
result.put("username", user.get("username"));
|
||||
result.put("updated", true);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically audited controller method with DeleteMapping.
|
||||
*/
|
||||
@DeleteMapping("/users/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteUser(@PathVariable String id) {
|
||||
// This method will be automatically audited by the ControllerAuditAspect
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("id", id);
|
||||
result.put("deleted", true);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
@ -66,8 +66,10 @@ public class InitialSecuritySetup {
|
||||
}
|
||||
|
||||
userService.saveAll(usersWithoutTeam); // batch save
|
||||
if(usersWithoutTeam != null && !usersWithoutTeam.isEmpty()) {
|
||||
log.info(
|
||||
"Assigned {} user(s) without a team to the default team.", usersWithoutTeam.size());
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeAdminUser() throws SQLException, UnsupportedProviderException {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@ -13,22 +14,34 @@ public final class SecretMasker {
|
||||
|
||||
private SecretMasker() {}
|
||||
|
||||
public static Map<String, Object> mask(Map<String, Object> in) {
|
||||
if (in == null) {
|
||||
return null;
|
||||
public 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())
|
||||
));
|
||||
} else if (value instanceof List<?> list) {
|
||||
return list.stream()
|
||||
.map(SecretMasker::deepMask)
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>(in.size());
|
||||
|
||||
for (Map.Entry<String, Object> entry : in.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (key != null && SENSITIVE.matcher(key).find()) {
|
||||
result.put(key, "***REDACTED***");
|
||||
} else {
|
||||
result.put(key, entry.getValue());
|
||||
}
|
||||
private static Object deepMaskValue(String key, Object value) {
|
||||
if (key != null && SENSITIVE.matcher(key).find()) {
|
||||
return "***REDACTED***";
|
||||
}
|
||||
return deepMask(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
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())
|
||||
));
|
||||
}
|
||||
}
|
@ -175,7 +175,6 @@ public class SPDFApplication {
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("Running configs {}", applicationProperties.toString());
|
||||
}
|
||||
|
||||
public static void setServerPortStatic(String port) {
|
||||
@ -208,20 +207,19 @@ public class SPDFApplication {
|
||||
if (arg.startsWith("--spring.profiles.active=")) {
|
||||
String[] provided = arg.substring(arg.indexOf('=') + 1).split(",");
|
||||
if (provided.length > 0) {
|
||||
log.info("#######0000000000000###############################");
|
||||
return provided;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("######################################");
|
||||
|
||||
// 2. Detect if SecurityConfiguration is present on classpath
|
||||
if (isClassPresent(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
|
||||
log.info("security");
|
||||
log.info("Additional features in jar");
|
||||
return new String[] {"security"};
|
||||
} else {
|
||||
log.info("default");
|
||||
log.info("Without additional features in jar");
|
||||
return new String[] {"default"};
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user