diff --git a/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java b/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java index 6d6fc2199..edf39a080 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java +++ b/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java @@ -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(); - - // Don't audit methods that already have @Audited annotation - if (method.isAnnotationPresent(Audited.class)) { - return joinPoint.proceed(); - } - - // Get the request path +// // Opt-out +// if (method.isAnnotationPresent(Audited.class)) { +// return joinPoint.proceed(); +// } + 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 auditData = new HashMap<>(); - auditData.put("controller", joinPoint.getTarget().getClass().getSimpleName()); - auditData.put("method", method.getName()); - auditData.put("httpMethod", httpMethod); - auditData.put("path", path); - - // Add method parameters if at VERBOSE level - if (auditConfig.isLevelEnabled(AuditLevel.VERBOSE)) { - Object[] args = joinPoint.getArgs(); - String[] parameterNames = signature.getParameterNames(); + + ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest req = attrs != null ? attrs.getRequest() : null; + HttpServletResponse resp = attrs != null ? attrs.getResponse() : null; + + long start = System.currentTimeMillis(); + Map 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 (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]); - }); + 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 params = req.getParameterMap(); + if (!params.isEmpty()) { + data.put("formParams", params); + } + } + + List files = Arrays.stream(joinPoint.getArgs()) + .filter(a -> a instanceof MultipartFile) + .map(a -> (MultipartFile)a) + .collect(Collectors.toList()); + + if (!files.isEmpty()) { + List> fileInfos = files.stream().map(f -> { + Map 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; } -} \ No newline at end of file +} diff --git a/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java b/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java index f705efca8..2926b9e89 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java +++ b/proprietary/src/main/java/stirling/software/proprietary/config/AsyncConfig.java @@ -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 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; } diff --git a/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java b/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java index 53950a8f0..2e3e04ab7 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java +++ b/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java @@ -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); diff --git a/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java b/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java index 860ab7400..5fa9367f9 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java +++ b/proprietary/src/main/java/stirling/software/proprietary/config/CustomAuditEventRepository.java @@ -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); diff --git a/proprietary/src/main/java/stirling/software/proprietary/config/HttpRequestAuditPublisher.java b/proprietary/src/main/java/stirling/software/proprietary/config/HttpRequestAuditPublisher.java index d8c604b00..e69de29bb 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/config/HttpRequestAuditPublisher.java +++ b/proprietary/src/main/java/stirling/software/proprietary/config/HttpRequestAuditPublisher.java @@ -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 { - - 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 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 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"; - } - } -} \ No newline at end of file diff --git a/proprietary/src/main/java/stirling/software/proprietary/controller/AuditExampleController.java b/proprietary/src/main/java/stirling/software/proprietary/controller/AuditExampleController.java deleted file mode 100644 index 270c24f6a..000000000 --- a/proprietary/src/main/java/stirling/software/proprietary/controller/AuditExampleController.java +++ /dev/null @@ -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> createUser(@RequestBody Map user) { - // This method is automatically audited with the USER_REGISTRATION type at BASIC level - - Map 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> 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 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> 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 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> updateUser( - @PathVariable String id, - @RequestBody Map user) { - // This method will be automatically audited by the ControllerAuditAspect - - Map 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> deleteUser(@PathVariable String id) { - // This method will be automatically audited by the ControllerAuditAspect - - Map result = new HashMap<>(); - result.put("id", id); - result.put("deleted", true); - - return ResponseEntity.ok(result); - } -} \ No newline at end of file diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java b/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java index 23f1100e6..ffe115f23 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java +++ b/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java @@ -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 { diff --git a/proprietary/src/main/java/stirling/software/proprietary/util/SecretMasker.java b/proprietary/src/main/java/stirling/software/proprietary/util/SecretMasker.java index 00079fd02..56d46710d 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/util/SecretMasker.java +++ b/proprietary/src/main/java/stirling/software/proprietary/util/SecretMasker.java @@ -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 mask(Map 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 result = new HashMap<>(in.size()); - - for (Map.Entry 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 result; + return deepMask(value); + } + + public static Map mask(Map in) { + if (in == null) return null; + return in.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> deepMaskValue(e.getKey(), e.getValue()) + )); } } \ No newline at end of file diff --git a/stirling-pdf/src/main/java/stirling/software/SPDF/SPDFApplication.java b/stirling-pdf/src/main/java/stirling/software/SPDF/SPDFApplication.java index cd356e8da..27bef32e4 100644 --- a/stirling-pdf/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/stirling-pdf/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -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"}; } }