mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
style: improve formatting and import order consistency across codebase (#3761)
# Description of Changes This pull request applies consistent formatting and import ordering across the codebase. Specifically: - Reordered imports according to the configured Spotless `importOrder()` directive. - Enabled formatting flags such as `trimTrailingWhitespace`, `leadingTabsToSpaces`, and `endWithNewline`. - Resolved inconsistencies in blank lines and spacing between imports and annotations. - Applied consistent formatting to annotations and method declarations. - Removed unused or redundant import statements. This change improves code readability, enforces a consistent style, and prepares the codebase for future automated formatting checks. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -7,7 +7,13 @@ bootRun {
|
||||
spotless {
|
||||
java {
|
||||
target sourceSets.main.allJava
|
||||
googleJavaFormat(googleJavaFormatVersion).aosp()
|
||||
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
||||
|
||||
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
||||
toggleOffOn()
|
||||
trimTrailingWhitespace()
|
||||
leadingTabsToSpaces()
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package stirling.software.proprietary.audit;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
@@ -10,18 +11,17 @@ 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;
|
||||
|
||||
/**
|
||||
* Aspect for processing {@link Audited} annotations.
|
||||
*/
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.service.AuditService;
|
||||
|
||||
/** Aspect for processing {@link Audited} annotations. */
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
@@ -36,18 +36,20 @@ public class AuditAspect {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||
|
||||
|
||||
// Fast path: use unified check to determine if we should audit
|
||||
// This avoids all data collection if auditing is disabled
|
||||
if (!AuditUtils.shouldAudit(method, auditConfig)) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
|
||||
// Only create the map once we know we'll use it
|
||||
Map<String, Object> auditData = AuditUtils.createBaseAuditData(joinPoint, auditedAnnotation.level());
|
||||
|
||||
Map<String, Object> auditData =
|
||||
AuditUtils.createBaseAuditData(joinPoint, auditedAnnotation.level());
|
||||
|
||||
// Add HTTP information if we're in a web context
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs != null) {
|
||||
HttpServletRequest req = attrs.getRequest();
|
||||
String path = req.getRequestURI();
|
||||
@@ -55,51 +57,55 @@ public class AuditAspect {
|
||||
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() &&
|
||||
(auditedAnnotation.level() == AuditLevel.VERBOSE ||
|
||||
auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
||||
|
||||
boolean includeArgs =
|
||||
auditedAnnotation.includeArgs()
|
||||
&& (auditedAnnotation.level() == AuditLevel.VERBOSE
|
||||
|| auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
||||
|
||||
if (includeArgs) {
|
||||
AuditUtils.addMethodArguments(auditData, joinPoint, AuditLevel.VERBOSE);
|
||||
}
|
||||
|
||||
|
||||
// Record start time for latency calculation
|
||||
long startTime = System.currentTimeMillis();
|
||||
Object result;
|
||||
try {
|
||||
// Execute the method
|
||||
result = joinPoint.proceed();
|
||||
|
||||
|
||||
// Add success status
|
||||
auditData.put("status", "success");
|
||||
|
||||
|
||||
// Add result if requested and if at VERBOSE level
|
||||
boolean includeResult = auditedAnnotation.includeResult() &&
|
||||
(auditedAnnotation.level() == AuditLevel.VERBOSE ||
|
||||
auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
||||
|
||||
boolean includeResult =
|
||||
auditedAnnotation.includeResult()
|
||||
&& (auditedAnnotation.level() == AuditLevel.VERBOSE
|
||||
|| auditConfig.getAuditLevel() == AuditLevel.VERBOSE);
|
||||
|
||||
if (includeResult && result != null) {
|
||||
// Use safe string conversion with size limiting
|
||||
auditData.put("result", AuditUtils.safeToString(result, 1000));
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (Throwable ex) {
|
||||
// Always add failure information regardless of level
|
||||
auditData.put("status", "failure");
|
||||
auditData.put("errorType", ex.getClass().getName());
|
||||
auditData.put("errorMessage", ex.getMessage());
|
||||
|
||||
|
||||
// Re-throw the exception
|
||||
throw ex;
|
||||
} finally {
|
||||
// Add timing information - use isHttpRequest=false to ensure we get timing for non-HTTP methods
|
||||
// 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);
|
||||
|
||||
AuditUtils.addTimingData(
|
||||
auditData, startTime, resp, auditedAnnotation.level(), isHttpRequest);
|
||||
|
||||
// Resolve the event type based on annotation and context
|
||||
String httpMethod = null;
|
||||
String path = null;
|
||||
@@ -108,15 +114,15 @@ public class AuditAspect {
|
||||
httpMethod = req.getMethod();
|
||||
path = req.getRequestURI();
|
||||
}
|
||||
|
||||
AuditEventType eventType = AuditUtils.resolveEventType(
|
||||
method,
|
||||
joinPoint.getTarget().getClass(),
|
||||
path,
|
||||
httpMethod,
|
||||
auditedAnnotation
|
||||
);
|
||||
|
||||
|
||||
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)) {
|
||||
@@ -128,4 +134,4 @@ public class AuditAspect {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,41 @@
|
||||
package stirling.software.proprietary.audit;
|
||||
|
||||
/**
|
||||
* Standardized audit event types for the application.
|
||||
*/
|
||||
/** Standardized audit event types for the application. */
|
||||
public enum AuditEventType {
|
||||
// Authentication events - BASIC level
|
||||
USER_LOGIN("User login"),
|
||||
USER_LOGOUT("User logout"),
|
||||
USER_LOGOUT("User logout"),
|
||||
USER_FAILED_LOGIN("Failed login attempt"),
|
||||
|
||||
|
||||
// User/admin events - BASIC level
|
||||
USER_PROFILE_UPDATE("User or profile operation"),
|
||||
|
||||
|
||||
// System configuration events - STANDARD level
|
||||
SETTINGS_CHANGED("System or admin settings operation"),
|
||||
|
||||
|
||||
// File operations - STANDARD level
|
||||
FILE_OPERATION("File operation"),
|
||||
|
||||
|
||||
// PDF operations - STANDARD level
|
||||
PDF_PROCESS("PDF processing operation"),
|
||||
|
||||
|
||||
// HTTP requests - STANDARD level
|
||||
HTTP_REQUEST("HTTP request");
|
||||
|
||||
|
||||
private final String description;
|
||||
|
||||
|
||||
AuditEventType(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the enum value from a string representation.
|
||||
* Useful for backward compatibility with string-based event types.
|
||||
*
|
||||
* Get the enum value from a string representation. Useful for backward compatibility with
|
||||
* string-based event types.
|
||||
*
|
||||
* @param type The string representation of the event type
|
||||
* @return The corresponding enum value or null if not found
|
||||
*/
|
||||
@@ -45,18 +43,18 @@ public enum AuditEventType {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
return AuditEventType.valueOf(type);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If the exact enum name doesn't match, try finding a similar one
|
||||
for (AuditEventType eventType : values()) {
|
||||
if (eventType.name().equalsIgnoreCase(type) ||
|
||||
eventType.getDescription().equalsIgnoreCase(type)) {
|
||||
if (eventType.name().equalsIgnoreCase(type)
|
||||
|| eventType.getDescription().equalsIgnoreCase(type)) {
|
||||
return eventType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +1,69 @@
|
||||
package stirling.software.proprietary.audit;
|
||||
|
||||
/**
|
||||
* Defines the different levels of audit logging available in the application.
|
||||
*/
|
||||
/** Defines the different levels of audit logging available in the application. */
|
||||
public enum AuditLevel {
|
||||
/**
|
||||
* OFF - No audit logging (level 0)
|
||||
* Disables all audit logging except for critical security events
|
||||
* OFF - No audit logging (level 0) Disables all audit logging except for critical security
|
||||
* events
|
||||
*/
|
||||
OFF(0),
|
||||
|
||||
|
||||
/**
|
||||
* BASIC - Minimal audit logging (level 1)
|
||||
* Includes:
|
||||
* - Authentication events (login, logout, failed logins)
|
||||
* - Password changes
|
||||
* - User/role changes
|
||||
* - System configuration changes
|
||||
* BASIC - Minimal audit logging (level 1) Includes: - Authentication events (login, logout,
|
||||
* failed logins) - Password changes - User/role changes - System configuration changes
|
||||
*/
|
||||
BASIC(1),
|
||||
|
||||
|
||||
/**
|
||||
* STANDARD - Standard audit logging (level 2)
|
||||
* Includes everything in BASIC plus:
|
||||
* - All HTTP requests (basic info: URL, method, status)
|
||||
* - File operations (upload, download, process)
|
||||
* - PDF operations (view, edit, etc.)
|
||||
* - User operations
|
||||
* STANDARD - Standard audit logging (level 2) Includes everything in BASIC plus: - All HTTP
|
||||
* requests (basic info: URL, method, status) - File operations (upload, download, process) -
|
||||
* PDF operations (view, edit, etc.) - User operations
|
||||
*/
|
||||
STANDARD(2),
|
||||
|
||||
|
||||
/**
|
||||
* VERBOSE - Detailed audit logging (level 3)
|
||||
* Includes everything in STANDARD plus:
|
||||
* - Request headers and parameters
|
||||
* - Method parameters
|
||||
* - Operation results
|
||||
* - Detailed timing information
|
||||
* VERBOSE - Detailed audit logging (level 3) Includes everything in STANDARD plus: - Request
|
||||
* headers and parameters - Method parameters - Operation results - Detailed timing information
|
||||
*/
|
||||
VERBOSE(3);
|
||||
|
||||
|
||||
private final int level;
|
||||
|
||||
|
||||
AuditLevel(int level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if this audit level includes the specified level
|
||||
*
|
||||
* @param otherLevel The level to check against
|
||||
* @return true if this level is equal to or greater than the specified level
|
||||
*/
|
||||
public boolean includes(AuditLevel otherLevel) {
|
||||
return this.level >= otherLevel.level;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get an AuditLevel from an integer value
|
||||
*
|
||||
* @param level The integer level (0-3)
|
||||
* @return The corresponding AuditLevel
|
||||
*/
|
||||
public static AuditLevel fromInt(int level) {
|
||||
// Ensure level is within valid bounds
|
||||
int boundedLevel = Math.min(Math.max(level, 0), 3);
|
||||
|
||||
|
||||
for (AuditLevel auditLevel : values()) {
|
||||
if (auditLevel.level == boundedLevel) {
|
||||
return auditLevel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Default to STANDARD if somehow we didn't match
|
||||
return STANDARD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
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;
|
||||
@@ -24,10 +10,26 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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 jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Shared utilities for audit aspects to ensure consistent behavior
|
||||
* across different audit mechanisms.
|
||||
* Shared utilities for audit aspects to ensure consistent behavior across different audit
|
||||
* mechanisms.
|
||||
*/
|
||||
@Slf4j
|
||||
public class AuditUtils {
|
||||
@@ -39,12 +41,13 @@ public class AuditUtils {
|
||||
* @param auditLevel The current audit level
|
||||
* @return A map with standard audit data
|
||||
*/
|
||||
public static Map<String, Object> createBaseAuditData(ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
||||
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) {
|
||||
@@ -52,16 +55,18 @@ public class AuditUtils {
|
||||
} 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());
|
||||
data.put(
|
||||
"methodName",
|
||||
((MethodSignature) joinPoint.getSignature()).getMethod().getName());
|
||||
}
|
||||
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add HTTP-specific information to the audit data if available
|
||||
*
|
||||
@@ -70,45 +75,50 @@ public class AuditUtils {
|
||||
* @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) {
|
||||
public static void addHttpData(
|
||||
Map<String, Object> data, String httpMethod, String path, AuditLevel auditLevel) {
|
||||
if (httpMethod == null || path == null) {
|
||||
return; // Skip if we don't have basic HTTP info
|
||||
}
|
||||
|
||||
|
||||
// BASIC level HTTP data
|
||||
data.put("httpMethod", httpMethod);
|
||||
data.put("path", path);
|
||||
|
||||
|
||||
// Get request attributes safely
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs == null) {
|
||||
return; // No request context available
|
||||
}
|
||||
|
||||
|
||||
HttpServletRequest req = attrs.getRequest();
|
||||
if (req == null) {
|
||||
return; // No request available
|
||||
}
|
||||
|
||||
|
||||
// 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(
|
||||
"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) {
|
||||
|
||||
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")) {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -116,7 +126,7 @@ public class AuditUtils {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add file information to the audit data if available
|
||||
*
|
||||
@@ -124,27 +134,33 @@ public class AuditUtils {
|
||||
* @param joinPoint The AspectJ join point
|
||||
* @param auditLevel The current audit level
|
||||
*/
|
||||
public static void addFileData(Map<String, Object> data, ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
||||
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());
|
||||
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());
|
||||
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
|
||||
*
|
||||
@@ -152,28 +168,30 @@ public class AuditUtils {
|
||||
* @param joinPoint The AspectJ join point
|
||||
* @param auditLevel The current audit level
|
||||
*/
|
||||
public static void addMethodArguments(Map<String, Object> data, ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
||||
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 -> {
|
||||
if (vals[i] != null) {
|
||||
// Convert objects to safe string representation
|
||||
data.put("arg_" + names[i], safeToString(vals[i], 500));
|
||||
} else {
|
||||
data.put("arg_" + names[i], null);
|
||||
}
|
||||
});
|
||||
.forEach(
|
||||
i -> {
|
||||
if (vals[i] != null) {
|
||||
// Convert objects to safe string representation
|
||||
data.put("arg_" + names[i], safeToString(vals[i], 500));
|
||||
} else {
|
||||
data.put("arg_" + names[i], null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Safely convert an object to string with size limiting
|
||||
*
|
||||
*
|
||||
* @param obj The object to convert
|
||||
* @param maxLength Maximum length of the resulting string
|
||||
* @return A safe string representation, truncated if needed
|
||||
@@ -182,7 +200,7 @@ public class AuditUtils {
|
||||
if (obj == null) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
|
||||
String result;
|
||||
try {
|
||||
// Handle common types directly to avoid toString() overhead
|
||||
@@ -196,19 +214,19 @@ public class AuditUtils {
|
||||
// For complex objects, use toString but handle exceptions
|
||||
result = obj.toString();
|
||||
}
|
||||
|
||||
|
||||
// Truncate if necessary
|
||||
if (result != null && result.length() > maxLength) {
|
||||
return StringUtils.truncate(result, maxLength - 3) + "...";
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
// If toString() fails, return the class name
|
||||
return "[" + obj.getClass().getName() + " - toString() failed]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if a method should be audited based on config and annotation
|
||||
*
|
||||
@@ -221,17 +239,16 @@ public class AuditUtils {
|
||||
if (!auditConfig.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check for annotation override
|
||||
Audited auditedAnnotation = method.getAnnotation(Audited.class);
|
||||
AuditLevel requiredLevel = (auditedAnnotation != null)
|
||||
? auditedAnnotation.level()
|
||||
: AuditLevel.BASIC;
|
||||
|
||||
AuditLevel requiredLevel =
|
||||
(auditedAnnotation != null) ? auditedAnnotation.level() : AuditLevel.BASIC;
|
||||
|
||||
// Check if the required level is enabled
|
||||
return auditConfig.getAuditLevel().includes(requiredLevel);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add timing and response status data to the audit record
|
||||
*
|
||||
@@ -241,14 +258,19 @@ public class AuditUtils {
|
||||
* @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) {
|
||||
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) {
|
||||
try {
|
||||
@@ -259,7 +281,7 @@ public class AuditUtils {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolve the event type to use for auditing, considering annotations and context
|
||||
*
|
||||
@@ -270,34 +292,45 @@ public class AuditUtils {
|
||||
* @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) {
|
||||
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 && path != 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")) {
|
||||
|
||||
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")) {
|
||||
} else if (cls.contains("admin")
|
||||
|| path.startsWith("/admin")
|
||||
|| path.startsWith("/settings")) {
|
||||
return AuditEventType.SETTINGS_CHANGED;
|
||||
} else if (cls.contains("file") || path.startsWith("/file")
|
||||
} 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
|
||||
*
|
||||
@@ -306,17 +339,18 @@ public class AuditUtils {
|
||||
* @param auditConfig The audit configuration
|
||||
* @return The audit level to use
|
||||
*/
|
||||
public static AuditLevel getEffectiveAuditLevel(Method method, AuditLevel defaultLevel, AuditConfigurationProperties auditConfig) {
|
||||
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
|
||||
*
|
||||
@@ -326,42 +360,50 @@ public class AuditUtils {
|
||||
* @param httpMethod The HTTP method
|
||||
* @return The determined audit event type
|
||||
*/
|
||||
public static AuditEventType determineAuditEventType(Method method, Class<?> controller, String path, String httpMethod) {
|
||||
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")) {
|
||||
|
||||
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")) {
|
||||
} else if (cls.contains("admin")
|
||||
|| path.startsWith("/admin")
|
||||
|| path.startsWith("/settings")) {
|
||||
return AuditEventType.SETTINGS_CHANGED;
|
||||
} else if (cls.contains("file") || path.startsWith("/file")
|
||||
} 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();
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return attrs != null ? attrs.getRequest() : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a GET request is for a static resource
|
||||
*
|
||||
@@ -369,7 +411,8 @@ public class AuditUtils {
|
||||
* @return true if this is a static resource request
|
||||
*/
|
||||
public static boolean isStaticResourceRequest(HttpServletRequest request) {
|
||||
return request != null && !RequestUriUtils.isTrackableResource(
|
||||
request.getContextPath(), request.getRequestURI());
|
||||
return request != null
|
||||
&& !RequestUriUtils.isTrackableResource(
|
||||
request.getContextPath(), request.getRequestURI());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,61 +7,51 @@ import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation for methods that should be audited.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
*
|
||||
* <p>Usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Audited(type = AuditEventType.USER_REGISTRATION, level = AuditLevel.BASIC)
|
||||
* public void registerUser(String username) {
|
||||
* // Method implementation
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* For backward compatibility, string-based event types are still supported:
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Audited(typeString = "CUSTOM_EVENT_TYPE", level = AuditLevel.BASIC)
|
||||
* public void customOperation() {
|
||||
* // Method implementation
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* }</pre>
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Audited {
|
||||
|
||||
|
||||
/**
|
||||
* The type of audit event using the standardized AuditEventType enum.
|
||||
* This is the preferred way to specify the event type.
|
||||
*
|
||||
* If both type() and typeString() are specified, type() takes precedence.
|
||||
* The type of audit event using the standardized AuditEventType enum. This is the preferred way
|
||||
* to specify the event type.
|
||||
*
|
||||
* <p>If both type() and typeString() are specified, type() takes precedence.
|
||||
*/
|
||||
AuditEventType type() default AuditEventType.HTTP_REQUEST;
|
||||
|
||||
|
||||
/**
|
||||
* The type of audit event as a string (e.g., "FILE_UPLOAD", "USER_REGISTRATION").
|
||||
* Provided for backward compatibility and custom event types not in the enum.
|
||||
*
|
||||
* If both type() and typeString() are specified, type() takes precedence.
|
||||
* The type of audit event as a string (e.g., "FILE_UPLOAD", "USER_REGISTRATION"). Provided for
|
||||
* backward compatibility and custom event types not in the enum.
|
||||
*
|
||||
* <p>If both type() and typeString() are specified, type() takes precedence.
|
||||
*/
|
||||
String typeString() default "";
|
||||
|
||||
/**
|
||||
* The audit level at which this event should be logged
|
||||
*/
|
||||
|
||||
/** The audit level at which this event should be logged */
|
||||
AuditLevel level() default AuditLevel.STANDARD;
|
||||
|
||||
/**
|
||||
* Should method arguments be included in the audit event
|
||||
*/
|
||||
|
||||
/** Should method arguments be included in the audit event */
|
||||
boolean includeArgs() default true;
|
||||
|
||||
/**
|
||||
* Should the method return value be included in the audit event
|
||||
*/
|
||||
|
||||
/** Should the method return value be included in the audit event */
|
||||
boolean includeResult() default false;
|
||||
}
|
||||
}
|
||||
|
||||
+52
-56
@@ -1,7 +1,9 @@
|
||||
package stirling.software.proprietary.audit;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
@@ -19,17 +21,16 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.service.AuditService;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Aspect for automatically auditing controller methods with web mappings
|
||||
* (GetMapping, PostMapping, etc.)
|
||||
* Aspect for automatically auditing controller methods with web mappings (GetMapping, PostMapping,
|
||||
* etc.)
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@@ -40,65 +41,57 @@ public class ControllerAuditAspect {
|
||||
private final AuditService auditService;
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
|
||||
|
||||
@Around("execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))")
|
||||
@Around(
|
||||
"execution(* org.springframework.web.servlet.resource.ResourceHttpRequestHandler.handleRequest(..))")
|
||||
public Object auditStaticResource(ProceedingJoinPoint jp) throws Throwable {
|
||||
return auditController(jp, "GET");
|
||||
}
|
||||
/**
|
||||
* Intercept all methods with GetMapping annotation
|
||||
*/
|
||||
|
||||
/** Intercept all methods with GetMapping annotation */
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping)")
|
||||
public Object auditGetMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditController(joinPoint, "GET");
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept all methods with PostMapping annotation
|
||||
*/
|
||||
/** Intercept all methods with PostMapping annotation */
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
|
||||
public Object auditPostMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditController(joinPoint, "POST");
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept all methods with PutMapping annotation
|
||||
*/
|
||||
/** Intercept all methods with PutMapping annotation */
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.PutMapping)")
|
||||
public Object auditPutMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditController(joinPoint, "PUT");
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept all methods with DeleteMapping annotation
|
||||
*/
|
||||
/** Intercept all methods with DeleteMapping annotation */
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
|
||||
public Object auditDeleteMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditController(joinPoint, "DELETE");
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept all methods with PatchMapping annotation
|
||||
*/
|
||||
/** Intercept all methods with PatchMapping annotation */
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.PatchMapping)")
|
||||
public Object auditPatchMethod(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
return auditController(joinPoint, "PATCH");
|
||||
}
|
||||
|
||||
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod) throws Throwable {
|
||||
private Object auditController(ProceedingJoinPoint joinPoint, String httpMethod)
|
||||
throws Throwable {
|
||||
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = sig.getMethod();
|
||||
|
||||
|
||||
// Fast path: check if auditing is enabled before doing any work
|
||||
// This avoids all data collection if auditing is disabled
|
||||
if (!AuditUtils.shouldAudit(method, auditConfig)) {
|
||||
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
|
||||
@@ -115,21 +108,22 @@ public class ControllerAuditAspect {
|
||||
}
|
||||
}
|
||||
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
HttpServletRequest req = attrs != null ? attrs.getRequest() : null;
|
||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
|
||||
// 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)) {
|
||||
AuditUtils.addMethodArguments(data, joinPoint, level);
|
||||
@@ -150,34 +144,35 @@ public class ControllerAuditAspect {
|
||||
data.put("latencyMs", System.currentTimeMillis() - start);
|
||||
if (resp != null) data.put("statusCode", resp.getStatus());
|
||||
}
|
||||
|
||||
|
||||
// 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) {
|
||||
// Use safe string conversion with size limiting
|
||||
data.put("result", AuditUtils.safeToString(result, 1000));
|
||||
}
|
||||
|
||||
|
||||
// Resolve the event type using the unified method
|
||||
AuditEventType eventType = AuditUtils.resolveEventType(
|
||||
method,
|
||||
joinPoint.getTarget().getClass(),
|
||||
path,
|
||||
httpMethod,
|
||||
auditedAnnotation
|
||||
);
|
||||
|
||||
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)) {
|
||||
if (eventType == AuditEventType.HTTP_REQUEST
|
||||
&& StringUtils.isNotEmpty(typeString)) {
|
||||
auditService.audit(typeString, data, level);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Use the enum type
|
||||
auditService.audit(eventType, data, level);
|
||||
}
|
||||
@@ -191,14 +186,15 @@ public class ControllerAuditAspect {
|
||||
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;
|
||||
};
|
||||
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];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package stirling.software.proprietary.config;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -7,23 +10,20 @@ 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
|
||||
* 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
|
||||
@@ -47,11 +47,11 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-17
@@ -1,17 +1,18 @@
|
||||
package stirling.software.proprietary.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
|
||||
/**
|
||||
* Configuration properties for the audit system.
|
||||
* Reads values from the ApplicationProperties under premium.enterpriseFeatures.audit
|
||||
* Configuration properties for the audit system. Reads values from the ApplicationProperties under
|
||||
* premium.enterpriseFeatures.audit
|
||||
*/
|
||||
@Slf4j
|
||||
@Getter
|
||||
@@ -22,47 +23,53 @@ public class AuditConfigurationProperties {
|
||||
private final boolean enabled;
|
||||
private final int level;
|
||||
private final int retentionDays;
|
||||
|
||||
|
||||
public AuditConfigurationProperties(ApplicationProperties applicationProperties) {
|
||||
ApplicationProperties.Premium.EnterpriseFeatures.Audit auditConfig =
|
||||
applicationProperties.getPremium().getEnterpriseFeatures().getAudit();
|
||||
ApplicationProperties.Premium.EnterpriseFeatures.Audit auditConfig =
|
||||
applicationProperties.getPremium().getEnterpriseFeatures().getAudit();
|
||||
// Read values directly from configuration
|
||||
this.enabled = auditConfig.isEnabled();
|
||||
|
||||
|
||||
// Ensure level is within valid bounds (0-3)
|
||||
int configLevel = auditConfig.getLevel();
|
||||
this.level = Math.min(Math.max(configLevel, 0), 3);
|
||||
|
||||
|
||||
// Retention days (0 means infinite)
|
||||
this.retentionDays = auditConfig.getRetentionDays();
|
||||
|
||||
log.debug("Initialized audit configuration: enabled={}, level={}, retentionDays={} (0=infinite)",
|
||||
this.enabled, this.level, this.retentionDays);
|
||||
|
||||
log.debug(
|
||||
"Initialized audit configuration: enabled={}, level={}, retentionDays={} (0=infinite)",
|
||||
this.enabled,
|
||||
this.level,
|
||||
this.retentionDays);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the audit level as an enum
|
||||
*
|
||||
* @return The current AuditLevel
|
||||
*/
|
||||
public AuditLevel getAuditLevel() {
|
||||
return AuditLevel.fromInt(level);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the current audit level includes the specified level
|
||||
*
|
||||
* @param requiredLevel The level to check against
|
||||
* @return true if auditing is enabled and the current level includes the required level
|
||||
*/
|
||||
public boolean isLevelEnabled(AuditLevel requiredLevel) {
|
||||
return enabled && getAuditLevel().includes(requiredLevel);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the effective retention period in days
|
||||
*
|
||||
* @return The number of days to retain audit records, or -1 for infinite retention
|
||||
*/
|
||||
public int getEffectiveRetentionDays() {
|
||||
// 0 means infinite retention
|
||||
return retentionDays <= 0 ? -1 : retentionDays;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* Configuration to explicitly enable JPA repositories and scheduling for the audit system.
|
||||
*/
|
||||
/** Configuration to explicitly enable JPA repositories and scheduling for the audit system. */
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@EnableJpaRepositories(basePackages = "stirling.software.proprietary.repository")
|
||||
@@ -16,4 +14,4 @@ public class AuditJpaConfig {
|
||||
// This configuration enables JPA repositories in the specified package
|
||||
// and enables scheduling for audit cleanup tasks
|
||||
// No additional beans or methods needed
|
||||
}
|
||||
}
|
||||
|
||||
+27
-27
@@ -1,8 +1,8 @@
|
||||
package stirling.software.proprietary.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||
@@ -11,14 +11,16 @@ import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||
import stirling.software.proprietary.util.SecretMasker;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Primary
|
||||
@RequiredArgsConstructor
|
||||
@@ -40,35 +42,33 @@ public class CustomAuditEventRepository implements AuditEventRepository {
|
||||
public void add(AuditEvent ev) {
|
||||
try {
|
||||
Map<String, Object> clean =
|
||||
CollectionUtils.isEmpty(ev.getData())
|
||||
? Map.of()
|
||||
: SecretMasker.mask(ev.getData());
|
||||
CollectionUtils.isEmpty(ev.getData())
|
||||
? Map.of()
|
||||
: SecretMasker.mask(ev.getData());
|
||||
|
||||
|
||||
if (clean.isEmpty() ||
|
||||
(clean.size() == 1 && clean.containsKey("details"))) {
|
||||
return;
|
||||
}
|
||||
if (clean.isEmpty() || (clean.size() == 1 && clean.containsKey("details"))) {
|
||||
return;
|
||||
}
|
||||
String rid = MDC.get("requestId");
|
||||
|
||||
|
||||
|
||||
if (rid != null) {
|
||||
clean = new java.util.HashMap<>(clean);
|
||||
clean.put("requestId", rid);
|
||||
}
|
||||
|
||||
String auditEventData = mapper.writeValueAsString(clean);
|
||||
log.debug("AuditEvent data (JSON): {}",auditEventData);
|
||||
|
||||
PersistentAuditEvent ent = PersistentAuditEvent.builder()
|
||||
.principal(ev.getPrincipal())
|
||||
.type(ev.getType())
|
||||
.data(auditEventData)
|
||||
.timestamp(ev.getTimestamp())
|
||||
.build();
|
||||
log.debug("AuditEvent data (JSON): {}", auditEventData);
|
||||
|
||||
PersistentAuditEvent ent =
|
||||
PersistentAuditEvent.builder()
|
||||
.principal(ev.getPrincipal())
|
||||
.type(ev.getType())
|
||||
.data(auditEventData)
|
||||
.timestamp(ev.getTimestamp())
|
||||
.build();
|
||||
repo.save(ent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // fail-open
|
||||
e.printStackTrace(); // fail-open
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
+104
-96
@@ -33,8 +33,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
@@ -42,10 +44,7 @@ import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
/**
|
||||
* Controller for the audit dashboard.
|
||||
* Admin-only access.
|
||||
*/
|
||||
/** Controller for the audit dashboard. Admin-only access. */
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/audit")
|
||||
@@ -58,28 +57,24 @@ public class AuditDashboardController {
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* Display the audit dashboard.
|
||||
*/
|
||||
/** Display the audit dashboard. */
|
||||
@GetMapping
|
||||
public String showDashboard(Model model) {
|
||||
model.addAttribute("auditEnabled", auditConfig.isEnabled());
|
||||
model.addAttribute("auditLevel", auditConfig.getAuditLevel());
|
||||
model.addAttribute("auditLevelInt", auditConfig.getLevel());
|
||||
model.addAttribute("retentionDays", auditConfig.getRetentionDays());
|
||||
|
||||
|
||||
// Add audit level enum values for display
|
||||
model.addAttribute("auditLevels", AuditLevel.values());
|
||||
|
||||
|
||||
// Add audit event types for the dropdown
|
||||
model.addAttribute("auditEventTypes", AuditEventType.values());
|
||||
|
||||
|
||||
return "audit/dashboard";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit events data for the dashboard tables.
|
||||
*/
|
||||
|
||||
/** Get audit events data for the dashboard tables. */
|
||||
@GetMapping("/data")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getAuditData(
|
||||
@@ -87,11 +82,13 @@ public class AuditDashboardController {
|
||||
@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)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, HttpServletRequest request) {
|
||||
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate endDate,
|
||||
HttpServletRequest request) {
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").descending());
|
||||
Page<PersistentAuditEvent> events;
|
||||
@@ -102,7 +99,9 @@ public class AuditDashboardController {
|
||||
mode = "principal + type + startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByPrincipalAndTypeAndTimestampBetween(principal, type, start, end, pageable);
|
||||
events =
|
||||
auditRepository.findByPrincipalAndTypeAndTimestampBetween(
|
||||
principal, type, start, end, pageable);
|
||||
} else if (type != null && principal != null) {
|
||||
mode = "principal + type";
|
||||
events = auditRepository.findByPrincipalAndType(principal, type, pageable);
|
||||
@@ -115,7 +114,9 @@ public class AuditDashboardController {
|
||||
mode = "principal + startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByPrincipalAndTimestampBetween(principal, start, end, pageable);
|
||||
events =
|
||||
auditRepository.findByPrincipalAndTimestampBetween(
|
||||
principal, start, end, pageable);
|
||||
} else if (startDate != null && endDate != null) {
|
||||
mode = "startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
@@ -144,85 +145,93 @@ public class AuditDashboardController {
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get statistics for charts.
|
||||
*/
|
||||
/** Get statistics for charts. */
|
||||
@GetMapping("/stats")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getAuditStats(
|
||||
@RequestParam(value = "days", defaultValue = "7") int days) {
|
||||
|
||||
|
||||
// Get events from the last X days
|
||||
Instant startDate = Instant.now().minus(java.time.Duration.ofDays(days));
|
||||
List<PersistentAuditEvent> events = auditRepository.findByTimestampAfter(startDate);
|
||||
|
||||
|
||||
// Count events by type
|
||||
Map<String, Long> eventsByType = events.stream()
|
||||
.collect(Collectors.groupingBy(PersistentAuditEvent::getType, Collectors.counting()));
|
||||
|
||||
Map<String, Long> eventsByType =
|
||||
events.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(
|
||||
PersistentAuditEvent::getType, Collectors.counting()));
|
||||
|
||||
// Count events by principal
|
||||
Map<String, Long> eventsByPrincipal = events.stream()
|
||||
.collect(Collectors.groupingBy(PersistentAuditEvent::getPrincipal, Collectors.counting()));
|
||||
|
||||
Map<String, Long> eventsByPrincipal =
|
||||
events.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(
|
||||
PersistentAuditEvent::getPrincipal, Collectors.counting()));
|
||||
|
||||
// Count events by day
|
||||
Map<String, Long> eventsByDay = events.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
e -> LocalDateTime.ofInstant(e.getTimestamp(), ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
Collectors.counting()));
|
||||
|
||||
Map<String, Long> eventsByDay =
|
||||
events.stream()
|
||||
.collect(
|
||||
Collectors.groupingBy(
|
||||
e ->
|
||||
LocalDateTime.ofInstant(
|
||||
e.getTimestamp(),
|
||||
ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ISO_LOCAL_DATE),
|
||||
Collectors.counting()));
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("eventsByType", eventsByType);
|
||||
stats.put("eventsByPrincipal", eventsByPrincipal);
|
||||
stats.put("eventsByDay", eventsByDay);
|
||||
stats.put("totalEvents", events.size());
|
||||
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique event types from the database for filtering.
|
||||
*/
|
||||
|
||||
/** Get all unique event types from the database for filtering. */
|
||||
@GetMapping("/types")
|
||||
@ResponseBody
|
||||
public List<String> getAuditTypes() {
|
||||
// Get distinct event types from the database
|
||||
List<String> dbTypes = auditRepository.findDistinctEventTypes();
|
||||
|
||||
|
||||
// Include standard enum types in case they're not in the database yet
|
||||
List<String> enumTypes = Arrays.stream(AuditEventType.values())
|
||||
.map(AuditEventType::name)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<String> enumTypes =
|
||||
Arrays.stream(AuditEventType.values())
|
||||
.map(AuditEventType::name)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Combine both sources, remove duplicates, and sort
|
||||
Set<String> combinedTypes = new HashSet<>();
|
||||
combinedTypes.addAll(dbTypes);
|
||||
combinedTypes.addAll(enumTypes);
|
||||
|
||||
|
||||
return combinedTypes.stream().sorted().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Export audit data as CSV.
|
||||
*/
|
||||
|
||||
/** Export audit data as CSV. */
|
||||
@GetMapping("/export")
|
||||
public ResponseEntity<byte[]> exportAuditData(
|
||||
@RequestParam(value = "type", required = false) String type,
|
||||
@RequestParam(value = "principal", required = false) String principal,
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate endDate) {
|
||||
|
||||
// Get data with same filtering as getAuditData
|
||||
List<PersistentAuditEvent> events;
|
||||
|
||||
|
||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
||||
principal, type, start, end);
|
||||
events =
|
||||
auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
||||
principal, type, start, end);
|
||||
} else if (type != null && principal != null) {
|
||||
events = auditRepository.findAllByPrincipalAndTypeForExport(principal, type);
|
||||
} else if (type != null && startDate != null && endDate != null) {
|
||||
@@ -232,7 +241,9 @@ public class AuditDashboardController {
|
||||
} else if (principal != null && startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findAllByPrincipalAndTimestampBetweenForExport(principal, start, end);
|
||||
events =
|
||||
auditRepository.findAllByPrincipalAndTimestampBetweenForExport(
|
||||
principal, start, end);
|
||||
} else if (startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
@@ -244,13 +255,13 @@ public class AuditDashboardController {
|
||||
} else {
|
||||
events = auditRepository.findAll();
|
||||
}
|
||||
|
||||
|
||||
// Convert to CSV
|
||||
StringBuilder csv = new StringBuilder();
|
||||
csv.append("ID,Principal,Type,Timestamp,Data\n");
|
||||
|
||||
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
|
||||
|
||||
|
||||
for (PersistentAuditEvent event : events) {
|
||||
csv.append(event.getId()).append(",");
|
||||
csv.append(escapeCSV(event.getPrincipal())).append(",");
|
||||
@@ -258,39 +269,38 @@ public class AuditDashboardController {
|
||||
csv.append(formatter.format(event.getTimestamp())).append(",");
|
||||
csv.append(escapeCSV(event.getData())).append("\n");
|
||||
}
|
||||
|
||||
|
||||
byte[] csvBytes = csv.toString().getBytes();
|
||||
|
||||
|
||||
// Set up HTTP headers for download
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||
headers.setContentDispositionFormData("attachment", "audit_export.csv");
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.headers(headers)
|
||||
.body(csvBytes);
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(csvBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export audit data as JSON.
|
||||
*/
|
||||
|
||||
/** Export audit data as JSON. */
|
||||
@GetMapping("/export/json")
|
||||
public ResponseEntity<byte[]> exportAuditDataJson(
|
||||
@RequestParam(value = "type", required = false) String type,
|
||||
@RequestParam(value = "principal", required = false) String principal,
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
LocalDate endDate) {
|
||||
|
||||
// Get data with same filtering as getAuditData
|
||||
List<PersistentAuditEvent> events;
|
||||
|
||||
|
||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
||||
principal, type, start, end);
|
||||
events =
|
||||
auditRepository.findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
||||
principal, type, start, end);
|
||||
} else if (type != null && principal != null) {
|
||||
events = auditRepository.findAllByPrincipalAndTypeForExport(principal, type);
|
||||
} else if (type != null && startDate != null && endDate != null) {
|
||||
@@ -300,7 +310,9 @@ public class AuditDashboardController {
|
||||
} else if (principal != null && startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findAllByPrincipalAndTimestampBetweenForExport(principal, start, end);
|
||||
events =
|
||||
auditRepository.findAllByPrincipalAndTimestampBetweenForExport(
|
||||
principal, start, end);
|
||||
} else if (startDate != null && endDate != null) {
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
@@ -312,28 +324,24 @@ public class AuditDashboardController {
|
||||
} else {
|
||||
events = auditRepository.findAll();
|
||||
}
|
||||
|
||||
|
||||
// Convert to JSON
|
||||
try {
|
||||
byte[] jsonBytes = objectMapper.writeValueAsBytes(events);
|
||||
|
||||
|
||||
// Set up HTTP headers for download
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setContentDispositionFormData("attachment", "audit_export.json");
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.headers(headers)
|
||||
.body(jsonBytes);
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(jsonBytes);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Error serializing audit events to JSON", e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to escape CSV fields.
|
||||
*/
|
||||
|
||||
/** Helper method to escape CSV fields. */
|
||||
private String escapeCSV(String field) {
|
||||
if (field == null) {
|
||||
return "";
|
||||
@@ -341,4 +349,4 @@ public class AuditDashboardController {
|
||||
// Replace double quotes with two double quotes and wrap in quotes
|
||||
return "\"" + field.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package stirling.software.proprietary.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
|
||||
@Entity
|
||||
|
||||
+22
-17
@@ -1,23 +1,29 @@
|
||||
package stirling.software.proprietary.model.security;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.Index;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "audit_events",
|
||||
indexes = {
|
||||
@jakarta.persistence.Index(name = "idx_audit_timestamp", columnList = "timestamp"),
|
||||
@jakarta.persistence.Index(name = "idx_audit_principal", columnList = "principal"),
|
||||
@jakarta.persistence.Index(name = "idx_audit_type", columnList = "type"),
|
||||
@jakarta.persistence.Index(name = "idx_audit_principal_type", columnList = "principal,type"),
|
||||
@jakarta.persistence.Index(name = "idx_audit_type_timestamp", columnList = "type,timestamp")
|
||||
}
|
||||
)
|
||||
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||
name = "audit_events",
|
||||
indexes = {
|
||||
@jakarta.persistence.Index(name = "idx_audit_timestamp", columnList = "timestamp"),
|
||||
@jakarta.persistence.Index(name = "idx_audit_principal", columnList = "principal"),
|
||||
@jakarta.persistence.Index(name = "idx_audit_type", columnList = "type"),
|
||||
@jakarta.persistence.Index(
|
||||
name = "idx_audit_principal_type",
|
||||
columnList = "principal,type"),
|
||||
@jakarta.persistence.Index(
|
||||
name = "idx_audit_type_timestamp",
|
||||
columnList = "type,timestamp")
|
||||
})
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PersistentAuditEvent {
|
||||
|
||||
@Id
|
||||
@@ -27,8 +33,7 @@ public class PersistentAuditEvent {
|
||||
private String principal;
|
||||
private String type;
|
||||
|
||||
@Lob
|
||||
private String data; // JSON blob
|
||||
@Lob private String data; // JSON blob
|
||||
|
||||
private Instant timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
+76
-30
@@ -15,58 +15,104 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
|
||||
@Repository
|
||||
public interface PersistentAuditEventRepository
|
||||
extends JpaRepository<PersistentAuditEvent, Long> {
|
||||
|
||||
public interface PersistentAuditEventRepository extends JpaRepository<PersistentAuditEvent, Long> {
|
||||
|
||||
// Basic queries
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
||||
Page<PersistentAuditEvent> findByPrincipal(@Param("principal") String principal, Pageable pageable);
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
||||
Page<PersistentAuditEvent> findByPrincipal(
|
||||
@Param("principal") String principal, Pageable pageable);
|
||||
|
||||
Page<PersistentAuditEvent> findByType(String type, Pageable pageable);
|
||||
Page<PersistentAuditEvent> findByTimestampBetween(Instant startDate, Instant endDate, Pageable pageable);
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndType(@Param("principal") String principal, @Param("type") String type, Pageable pageable);
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndTimestampBetween(@Param("principal") String principal, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate, Pageable pageable);
|
||||
Page<PersistentAuditEvent> findByTypeAndTimestampBetween(String type, Instant startDate, Instant endDate, Pageable pageable);
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndTypeAndTimestampBetween(@Param("principal") String principal, @Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate, Pageable pageable);
|
||||
|
||||
|
||||
Page<PersistentAuditEvent> findByTimestampBetween(
|
||||
Instant startDate, Instant endDate, Pageable pageable);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndType(
|
||||
@Param("principal") String principal, @Param("type") String type, Pageable pageable);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndTimestampBetween(
|
||||
@Param("principal") String principal,
|
||||
@Param("startDate") Instant startDate,
|
||||
@Param("endDate") Instant endDate,
|
||||
Pageable pageable);
|
||||
|
||||
Page<PersistentAuditEvent> findByTypeAndTimestampBetween(
|
||||
String type, Instant startDate, Instant endDate, Pageable pageable);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
Page<PersistentAuditEvent> findByPrincipalAndTypeAndTimestampBetween(
|
||||
@Param("principal") String principal,
|
||||
@Param("type") String type,
|
||||
@Param("startDate") Instant startDate,
|
||||
@Param("endDate") Instant endDate,
|
||||
Pageable pageable);
|
||||
|
||||
// Non-paged versions for export
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%'))")
|
||||
List<PersistentAuditEvent> findAllByPrincipalForExport(@Param("principal") String principal);
|
||||
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type")
|
||||
List<PersistentAuditEvent> findByTypeForExport(@Param("type") String type);
|
||||
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.timestamp BETWEEN :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByTimestampBetweenForExport(@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
List<PersistentAuditEvent> findAllByTimestampBetweenForExport(
|
||||
@Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.timestamp > :startDate")
|
||||
List<PersistentAuditEvent> findByTimestampAfter(@Param("startDate") Instant startDate);
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeForExport(@Param("principal") String principal, @Param("type") String type);
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTimestampBetweenForExport(@Param("principal") String principal, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByTypeAndTimestampBetweenForExport(@Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeAndTimestampBetweenForExport(@Param("principal") String principal, @Param("type") String type, @Param("startDate") Instant startDate, @Param("endDate") Instant endDate);
|
||||
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeForExport(
|
||||
@Param("principal") String principal, @Param("type") String type);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTimestampBetweenForExport(
|
||||
@Param("principal") String principal,
|
||||
@Param("startDate") Instant startDate,
|
||||
@Param("endDate") Instant endDate);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByTypeAndTimestampBetweenForExport(
|
||||
@Param("type") String type,
|
||||
@Param("startDate") Instant startDate,
|
||||
@Param("endDate") Instant endDate);
|
||||
|
||||
@Query(
|
||||
"SELECT e FROM PersistentAuditEvent e WHERE UPPER(e.principal) LIKE UPPER(CONCAT('%', :principal, '%')) AND e.type = :type AND e.timestamp BETWEEN :startDate AND :endDate")
|
||||
List<PersistentAuditEvent> findAllByPrincipalAndTypeAndTimestampBetweenForExport(
|
||||
@Param("principal") String principal,
|
||||
@Param("type") String type,
|
||||
@Param("startDate") Instant startDate,
|
||||
@Param("endDate") Instant endDate);
|
||||
|
||||
// Cleanup queries
|
||||
@Query("DELETE FROM PersistentAuditEvent e WHERE e.timestamp < ?1")
|
||||
@Modifying
|
||||
@Transactional
|
||||
int deleteByTimestampBefore(Instant cutoffDate);
|
||||
|
||||
|
||||
// Find IDs for batch deletion - using JPQL with setMaxResults instead of native query
|
||||
@Query("SELECT e.id FROM PersistentAuditEvent e WHERE e.timestamp < ?1 ORDER BY e.id")
|
||||
List<Long> findIdsForBatchDeletion(Instant cutoffDate, Pageable pageable);
|
||||
|
||||
|
||||
// Stats queries
|
||||
@Query("SELECT e.type, COUNT(e) FROM PersistentAuditEvent e GROUP BY e.type")
|
||||
List<Object[]> countByType();
|
||||
|
||||
|
||||
@Query("SELECT e.principal, COUNT(e) FROM PersistentAuditEvent e GROUP BY e.principal")
|
||||
List<Object[]> countByPrincipal();
|
||||
|
||||
|
||||
// Get distinct event types for filtering
|
||||
@Query("SELECT DISTINCT e.type FROM PersistentAuditEvent e ORDER BY e.type")
|
||||
List<String> findDistinctEventTypes();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -1,13 +1,8 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||
|
||||
+3
-6
@@ -1,7 +1,6 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
@@ -11,11 +10,9 @@ import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.io.IOException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
|
||||
+11
-6
@@ -1,22 +1,27 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import com.coveo.saml.SamlClient;
|
||||
import com.coveo.saml.SamlException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
|
||||
import com.coveo.saml.SamlClient;
|
||||
import com.coveo.saml.SamlException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
|
||||
+10
-5
@@ -1,13 +1,17 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
@@ -62,9 +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());
|
||||
if (usersWithoutTeam != null && !usersWithoutTeam.isEmpty()) {
|
||||
log.info(
|
||||
"Assigned {} user(s) without a team to the default team.",
|
||||
usersWithoutTeam.size());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
package stirling.software.proprietary.security;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.security.filter.IPRateLimitingFilter;
|
||||
|
||||
@Component
|
||||
|
||||
+11
-5
@@ -2,10 +2,6 @@ package stirling.software.proprietary.security.config;
|
||||
|
||||
import static stirling.software.common.util.ProviderUtils.validateProvider;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
@@ -14,7 +10,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -23,6 +19,16 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
|
||||
+1
-1
@@ -8,4 +8,4 @@ import java.lang.annotation.Target;
|
||||
/** Annotation to mark endpoints that require an Enterprise license. */
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface EnterpriseEndpoint {}
|
||||
public @interface EnterpriseEndpoint {}
|
||||
|
||||
+1
-1
@@ -27,4 +27,4 @@ public class EnterpriseEndpointAspect {
|
||||
}
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -1,8 +1,7 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
@@ -12,6 +11,10 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
|
||||
+5
-2
@@ -1,13 +1,16 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
import java.util.Properties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
/**
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
package stirling.software.proprietary.security.configuration;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -27,6 +27,9 @@ import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.security.CustomAuthenticationFailureHandler;
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.EnterpriseEdition;
|
||||
import stirling.software.common.model.ApplicationProperties.Premium;
|
||||
|
||||
+10
-6
@@ -1,20 +1,24 @@
|
||||
package stirling.software.proprietary.security.configuration.ee;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.posthog.java.shaded.org.json.JSONException;
|
||||
import com.posthog.java.shaded.org.json.JSONObject;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.Base64;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
|
||||
import org.bouncycastle.crypto.signers.Ed25519Signer;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.posthog.java.shaded.org.json.JSONException;
|
||||
import com.posthog.java.shaded.org.json.JSONObject;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
|
||||
+4
-1
@@ -4,9 +4,12 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
|
||||
|
||||
+10
-6
@@ -1,17 +1,12 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
@@ -23,6 +18,15 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.security.database.H2SQLCondition;
|
||||
import stirling.software.proprietary.security.service.DatabaseService;
|
||||
|
||||
|
||||
+10
-6
@@ -1,11 +1,5 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -14,6 +8,16 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.security.model.api.Email;
|
||||
import stirling.software.proprietary.security.service.EmailService;
|
||||
|
||||
|
||||
+9
-4
@@ -1,14 +1,19 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.config.PremiumEndpoint;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
|
||||
+11
-6
@@ -1,17 +1,12 @@
|
||||
package stirling.software.proprietary.security.controller.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -25,6 +20,16 @@ import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
|
||||
+8
-3
@@ -1,14 +1,19 @@
|
||||
package stirling.software.proprietary.security.controller.web;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.FileInfo;
|
||||
import stirling.software.proprietary.security.service.DatabaseService;
|
||||
|
||||
|
||||
+7
-3
@@ -1,18 +1,22 @@
|
||||
package stirling.software.proprietary.security.controller.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
|
||||
import stirling.software.proprietary.security.database.repository.SessionRepository;
|
||||
|
||||
+4
-1
@@ -1,10 +1,13 @@
|
||||
package stirling.software.proprietary.security.database;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.proprietary.security.service.DatabaseServiceInterface;
|
||||
|
||||
|
||||
+2
@@ -1,8 +1,10 @@
|
||||
package stirling.software.proprietary.security.database.repository;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import stirling.software.proprietary.security.model.Authority;
|
||||
|
||||
@Repository
|
||||
|
||||
+2
@@ -1,9 +1,11 @@
|
||||
package stirling.software.proprietary.security.database.repository;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import stirling.software.proprietary.security.model.PersistentLogin;
|
||||
|
||||
public class JPATokenRepositoryImpl implements PersistentTokenRepository {
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ package stirling.software.proprietary.security.database.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import stirling.software.proprietary.security.model.PersistentLogin;
|
||||
|
||||
@Repository
|
||||
|
||||
+4
-1
@@ -1,13 +1,16 @@
|
||||
package stirling.software.proprietary.security.database.repository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import stirling.software.proprietary.security.model.SessionEntity;
|
||||
|
||||
@Repository
|
||||
|
||||
+2
@@ -2,10 +2,12 @@ package stirling.software.proprietary.security.database.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
|
||||
|
||||
+7
-5
@@ -1,14 +1,16 @@
|
||||
package stirling.software.proprietary.security.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@Component
|
||||
public class EnterpriseEndpointFilter extends OncePerRequestFilter {
|
||||
|
||||
+13
-9
@@ -1,20 +1,24 @@
|
||||
package stirling.software.proprietary.security.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
+6
-3
@@ -1,15 +1,18 @@
|
||||
package stirling.software.proprietary.security.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
|
||||
+9
-5
@@ -1,13 +1,9 @@
|
||||
package stirling.software.proprietary.security.filter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -20,6 +16,14 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
|
||||
+12
-8
@@ -1,17 +1,10 @@
|
||||
package stirling.software.proprietary.security.filter;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket;
|
||||
import io.github.bucket4j.ConsumptionProbe;
|
||||
import io.github.pixee.security.Newlines;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -20,6 +13,17 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket;
|
||||
import io.github.bucket4j.ConsumptionProbe;
|
||||
import io.github.pixee.security.Newlines;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
|
||||
@Component
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.security.model;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
package stirling.software.proprietary.security.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
@@ -8,7 +10,7 @@ import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
|
||||
+3
-1
@@ -1,10 +1,12 @@
|
||||
package stirling.software.proprietary.security.model;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Entity
|
||||
|
||||
+4
-2
@@ -1,10 +1,12 @@
|
||||
package stirling.software.proprietary.security.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Entity
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package stirling.software.proprietary.security.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.proprietary.model.Team;
|
||||
|
||||
|
||||
+4
-1
@@ -1,10 +1,13 @@
|
||||
package stirling.software.proprietary.security.model.api;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
|
||||
@Data
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.security.model.api.user;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.security.model.api.user;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.security.model.api.user;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
package stirling.software.proprietary.security.model.api.user;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
|
||||
+7
-4
@@ -1,10 +1,7 @@
|
||||
package stirling.software.proprietary.security.oauth2;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.DisabledException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
@@ -13,6 +10,12 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class CustomOAuth2AuthenticationFailureHandler
|
||||
extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
+9
-5
@@ -1,18 +1,22 @@
|
||||
package stirling.software.proprietary.security.oauth2;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
|
||||
+4
-1
@@ -9,7 +9,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -23,6 +23,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
|
||||
+2
@@ -2,9 +2,11 @@ package stirling.software.proprietary.security.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
|
||||
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ package stirling.software.proprietary.security.saml2;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
|
||||
|
||||
+6
-3
@@ -1,9 +1,7 @@
|
||||
package stirling.software.proprietary.security.saml2;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.authentication.ProviderNotFoundException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
@@ -11,6 +9,11 @@ import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
+10
-6
@@ -1,17 +1,21 @@
|
||||
package stirling.software.proprietary.security.saml2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
|
||||
+5
-2
@@ -5,8 +5,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
@@ -17,6 +16,10 @@ import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
import stirling.software.proprietary.security.service.UserService;
|
||||
|
||||
|
||||
+7
-3
@@ -1,11 +1,9 @@
|
||||
package stirling.software.proprietary.security.saml2;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -20,6 +18,12 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.SAML2;
|
||||
|
||||
|
||||
+4
-1
@@ -1,10 +1,13 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.configuration.interfaces.ShowAdminInterface;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||
@@ -10,6 +10,9 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||
|
||||
+4
-1
@@ -2,7 +2,7 @@ package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
@@ -10,6 +10,9 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.security.database.repository.UserRepository;
|
||||
import stirling.software.proprietary.security.model.Authority;
|
||||
import stirling.software.proprietary.security.model.User;
|
||||
|
||||
+5
-1
@@ -18,11 +18,15 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.jdbc.datasource.init.CannotReadScriptException;
|
||||
import org.springframework.jdbc.datasource.init.ScriptException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.FileInfo;
|
||||
|
||||
+1
@@ -2,6 +2,7 @@ package stirling.software.proprietary.security.service;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
import stirling.software.common.model.FileInfo;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
|
||||
|
||||
+6
-3
@@ -1,14 +1,17 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.security.model.api.Email;
|
||||
|
||||
|
||||
+6
-2
@@ -1,11 +1,15 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.proprietary.security.model.AttemptCounter;
|
||||
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.model.Team;
|
||||
import stirling.software.proprietary.security.repository.TeamRepository;
|
||||
|
||||
|
||||
+5
-2
@@ -9,8 +9,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
@@ -25,6 +24,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.enumeration.Role;
|
||||
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||
|
||||
+3
-1
@@ -1,9 +1,11 @@
|
||||
package stirling.software.proprietary.security.session;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpSessionEvent;
|
||||
import jakarta.servlet.http.HttpSessionListener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
|
||||
+6
-2
@@ -1,6 +1,5 @@
|
||||
package stirling.software.proprietary.security.session;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -8,13 +7,18 @@ import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.security.database.repository.SessionRepository;
|
||||
import stirling.software.proprietary.security.model.SessionEntity;
|
||||
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ package stirling.software.proprietary.security.session;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
|
||||
import stirling.software.proprietary.security.database.repository.SessionRepository;
|
||||
|
||||
@Configuration
|
||||
|
||||
+3
-1
@@ -4,11 +4,13 @@ import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SessionScheduled {
|
||||
|
||||
+27
-30
@@ -13,12 +13,11 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.repository.PersistentAuditEventRepository;
|
||||
|
||||
/**
|
||||
* Service to periodically clean up old audit events based on retention policy.
|
||||
*/
|
||||
/** Service to periodically clean up old audit events based on retention policy. */
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -26,87 +25,85 @@ public class AuditCleanupService {
|
||||
|
||||
private final PersistentAuditEventRepository auditRepository;
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
|
||||
|
||||
// Default batch size for deletions
|
||||
private static final int BATCH_SIZE = 10000;
|
||||
|
||||
|
||||
/**
|
||||
* Scheduled task that runs daily to clean up old audit events.
|
||||
* The retention period is configurable in settings.yml.
|
||||
* Scheduled task that runs daily to clean up old audit events. The retention period is
|
||||
* configurable in settings.yml.
|
||||
*/
|
||||
@Scheduled(fixedDelay = 1, initialDelay = 1, timeUnit = TimeUnit.DAYS)
|
||||
public void cleanupOldAuditEvents() {
|
||||
if (!auditConfig.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
int retentionDays = auditConfig.getRetentionDays();
|
||||
if (retentionDays <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
log.info("Starting audit cleanup for events older than {} days", retentionDays);
|
||||
|
||||
|
||||
try {
|
||||
Instant cutoffDate = Instant.now().minus(retentionDays, ChronoUnit.DAYS);
|
||||
int totalDeleted = batchDeleteEvents(cutoffDate);
|
||||
log.info("Successfully cleaned up {} audit events older than {}", totalDeleted, cutoffDate);
|
||||
log.info(
|
||||
"Successfully cleaned up {} audit events older than {}",
|
||||
totalDeleted,
|
||||
cutoffDate);
|
||||
} catch (Exception e) {
|
||||
log.error("Error cleaning up old audit events", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs batch deletion of events to prevent long-running transactions
|
||||
* and potential database locks.
|
||||
* Performs batch deletion of events to prevent long-running transactions and potential database
|
||||
* locks.
|
||||
*/
|
||||
private int batchDeleteEvents(Instant cutoffDate) {
|
||||
int totalDeleted = 0;
|
||||
boolean hasMore = true;
|
||||
|
||||
|
||||
while (hasMore) {
|
||||
// Start a new transaction for each batch
|
||||
List<Long> batchIds = findBatchOfIdsToDelete(cutoffDate);
|
||||
|
||||
|
||||
if (batchIds.isEmpty()) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
int deleted = deleteBatch(batchIds);
|
||||
totalDeleted += deleted;
|
||||
|
||||
|
||||
// If we got fewer records than the batch size, we're done
|
||||
if (batchIds.size() < BATCH_SIZE) {
|
||||
hasMore = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return totalDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a batch of IDs to delete.
|
||||
*/
|
||||
|
||||
/** Finds a batch of IDs to delete. */
|
||||
@Transactional(readOnly = true)
|
||||
private List<Long> findBatchOfIdsToDelete(Instant cutoffDate) {
|
||||
PageRequest pageRequest = PageRequest.of(0, BATCH_SIZE, Sort.by("id"));
|
||||
return auditRepository.findIdsForBatchDeletion(cutoffDate, pageRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a batch of events by ID.
|
||||
* Each batch is in its own transaction.
|
||||
*/
|
||||
|
||||
/** Deletes a batch of events by ID. Each batch is in its own transaction. */
|
||||
@Transactional
|
||||
private int deleteBatch(List<Long> batchIds) {
|
||||
if (batchIds.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int batchSize = batchIds.size();
|
||||
auditRepository.deleteAllByIdInBatch(batchIds);
|
||||
log.debug("Deleted batch of {} audit events", batchSize);
|
||||
|
||||
|
||||
return batchSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package stirling.software.proprietary.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.actuate.audit.AuditEvent;
|
||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service for creating manual audit events throughout the application.
|
||||
* This provides easy access to audit functionality in any component.
|
||||
* Service for creating manual audit events throughout the application. This provides easy access to
|
||||
* audit functionality in any component.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -24,18 +25,20 @@ public class AuditService {
|
||||
private final AuditEventRepository repository;
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
private final boolean runningEE;
|
||||
|
||||
public AuditService(AuditEventRepository repository,
|
||||
AuditConfigurationProperties auditConfig,
|
||||
@org.springframework.beans.factory.annotation.Qualifier("runningEE") boolean runningEE) {
|
||||
|
||||
public AuditService(
|
||||
AuditEventRepository repository,
|
||||
AuditConfigurationProperties auditConfig,
|
||||
@org.springframework.beans.factory.annotation.Qualifier("runningEE")
|
||||
boolean runningEE) {
|
||||
this.repository = repository;
|
||||
this.auditConfig = auditConfig;
|
||||
this.runningEE = runningEE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event for the current authenticated user with a specific audit level
|
||||
* using the standardized AuditEventType enum
|
||||
* Record an audit event for the current authenticated user with a specific audit level using
|
||||
* the standardized AuditEventType enum
|
||||
*
|
||||
* @param type The event type from AuditEventType enum
|
||||
* @param data Additional event data (will be automatically sanitized)
|
||||
@@ -43,17 +46,19 @@ public class AuditService {
|
||||
*/
|
||||
public void audit(AuditEventType type, Map<String, Object> data, AuditLevel level) {
|
||||
// Skip auditing if this level is not enabled or if not Enterprise edition
|
||||
if (!auditConfig.isEnabled() || !auditConfig.getAuditLevel().includes(level) || !runningEE) {
|
||||
if (!auditConfig.isEnabled()
|
||||
|| !auditConfig.getAuditLevel().includes(level)
|
||||
|| !runningEE) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String principal = getCurrentUsername();
|
||||
repository.add(new AuditEvent(principal, type.name(), data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event for the current authenticated user with standard level
|
||||
* using the standardized AuditEventType enum
|
||||
* Record an audit event for the current authenticated user with standard level using the
|
||||
* standardized AuditEventType enum
|
||||
*
|
||||
* @param type The event type from AuditEventType enum
|
||||
* @param data Additional event data (will be automatically sanitized)
|
||||
@@ -64,26 +69,27 @@ public class AuditService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event for a specific user with a specific audit level
|
||||
* using the standardized AuditEventType enum
|
||||
* Record an audit event for a specific user with a specific audit level using the standardized
|
||||
* AuditEventType enum
|
||||
*
|
||||
* @param principal The username or system identifier
|
||||
* @param type The event type from AuditEventType enum
|
||||
* @param data Additional event data (will be automatically sanitized)
|
||||
* @param level The minimum audit level required for this event to be logged
|
||||
*/
|
||||
public void audit(String principal, AuditEventType type, Map<String, Object> data, AuditLevel level) {
|
||||
public void audit(
|
||||
String principal, AuditEventType type, Map<String, Object> data, AuditLevel level) {
|
||||
// Skip auditing if this level is not enabled or if not Enterprise edition
|
||||
if (!auditConfig.isLevelEnabled(level) || !runningEE) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
repository.add(new AuditEvent(principal, type.name(), data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event for a specific user with standard level
|
||||
* using the standardized AuditEventType enum
|
||||
* Record an audit event for a specific user with standard level using the standardized
|
||||
* AuditEventType enum
|
||||
*
|
||||
* @param principal The username or system identifier
|
||||
* @param type The event type from AuditEventType enum
|
||||
@@ -95,8 +101,8 @@ public class AuditService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event for the current authenticated user with a specific audit level
|
||||
* using a string-based event type (for backward compatibility)
|
||||
* Record an audit event for the current authenticated user with a specific audit level using a
|
||||
* string-based event type (for backward compatibility)
|
||||
*
|
||||
* @param type The event type (e.g., "FILE_UPLOAD", "PASSWORD_CHANGE")
|
||||
* @param data Additional event data (will be automatically sanitized)
|
||||
@@ -107,14 +113,14 @@ public class AuditService {
|
||||
if (!auditConfig.isLevelEnabled(level) || !runningEE) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String principal = getCurrentUsername();
|
||||
repository.add(new AuditEvent(principal, type, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event for the current authenticated user with standard level
|
||||
* using a string-based event type (for backward compatibility)
|
||||
* Record an audit event for the current authenticated user with standard level using a
|
||||
* string-based event type (for backward compatibility)
|
||||
*
|
||||
* @param type The event type (e.g., "FILE_UPLOAD", "PASSWORD_CHANGE")
|
||||
* @param data Additional event data (will be automatically sanitized)
|
||||
@@ -125,8 +131,8 @@ public class AuditService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event for a specific user with a specific audit level
|
||||
* using a string-based event type (for backward compatibility)
|
||||
* Record an audit event for a specific user with a specific audit level using a string-based
|
||||
* event type (for backward compatibility)
|
||||
*
|
||||
* @param principal The username or system identifier
|
||||
* @param type The event type (e.g., "FILE_UPLOAD", "PASSWORD_CHANGE")
|
||||
@@ -138,13 +144,13 @@ public class AuditService {
|
||||
if (!auditConfig.isLevelEnabled(level) || !runningEE) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
repository.add(new AuditEvent(principal, type, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event for a specific user with standard level
|
||||
* using a string-based event type (for backward compatibility)
|
||||
* Record an audit event for a specific user with standard level using a string-based event type
|
||||
* (for backward compatibility)
|
||||
*
|
||||
* @param principal The username or system identifier
|
||||
* @param type The event type (e.g., "FILE_UPLOAD", "PASSWORD_CHANGE")
|
||||
@@ -155,11 +161,9 @@ public class AuditService {
|
||||
audit(principal, type, data, AuditLevel.STANDARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current authenticated username or "system" if none
|
||||
*/
|
||||
/** Get the current authenticated username or "system" if none */
|
||||
private String getCurrentUsername() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
return (auth != null && auth.getName() != null) ? auth.getName() : "system";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
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;
|
||||
|
||||
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. */
|
||||
@@ -17,45 +12,40 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public final class SecretMasker {
|
||||
|
||||
private static final Pattern SENSITIVE =
|
||||
Pattern.compile("(?i)(password|token|secret|api[_-]?key|authorization|auth|jwt|cred|cert)");
|
||||
Pattern.compile(
|
||||
"(?i)(password|token|secret|api[_-]?key|authorization|auth|jwt|cred|cert)");
|
||||
|
||||
private SecretMasker() {}
|
||||
|
||||
public static Map<String,Object> mask(Map<String,Object> in) {
|
||||
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())
|
||||
));
|
||||
.filter(e -> e.getValue() != null)
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
Map.Entry::getKey, e -> deepMaskValue(e.getKey(), e.getValue())));
|
||||
}
|
||||
|
||||
private static Object deepMask(Object value) {
|
||||
if (value instanceof Map<?,?> m) {
|
||||
if (value instanceof Map<?, ?> m) {
|
||||
return m.entrySet().stream()
|
||||
.filter(e -> e.getValue() != null)
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
e -> deepMaskValue((String)e.getKey(), e.getValue())
|
||||
));
|
||||
.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).toList();
|
||||
return list.stream().map(SecretMasker::deepMask).toList();
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Object deepMaskValue(String key, Object value) {
|
||||
if (key != null && SENSITIVE.matcher(key).find()) {
|
||||
return "***REDACTED***";
|
||||
}
|
||||
return deepMask(value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package stirling.software.proprietary.web;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@@ -13,18 +10,16 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Filter that stores additional request information for audit purposes
|
||||
*/
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/** Filter that stores additional request information for audit purposes */
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
|
||||
@@ -37,10 +32,10 @@ public class AuditWebFilter extends OncePerRequestFilter {
|
||||
private static final String CONTENT_TYPE_HEADER = "Content-Type";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// Store key request info in MDC for logging and later audit use
|
||||
try {
|
||||
// Store request headers
|
||||
@@ -48,42 +43,43 @@ public class AuditWebFilter extends OncePerRequestFilter {
|
||||
if (userAgent != null) {
|
||||
MDC.put("userAgent", userAgent);
|
||||
}
|
||||
|
||||
|
||||
String referer = request.getHeader(REFERER_HEADER);
|
||||
if (referer != null) {
|
||||
MDC.put("referer", referer);
|
||||
}
|
||||
|
||||
|
||||
String acceptLanguage = request.getHeader(ACCEPT_LANGUAGE_HEADER);
|
||||
if (acceptLanguage != null) {
|
||||
MDC.put("acceptLanguage", acceptLanguage);
|
||||
}
|
||||
|
||||
|
||||
String contentType = request.getHeader(CONTENT_TYPE_HEADER);
|
||||
if (contentType != null) {
|
||||
MDC.put("contentType", contentType);
|
||||
}
|
||||
|
||||
|
||||
// Store authenticated user roles if available
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.getAuthorities() != null) {
|
||||
String roles = auth.getAuthorities().stream()
|
||||
.map(a -> a.getAuthority())
|
||||
.reduce((a, b) -> a + "," + b)
|
||||
.orElse("");
|
||||
String roles =
|
||||
auth.getAuthorities().stream()
|
||||
.map(a -> a.getAuthority())
|
||||
.reduce((a, b) -> a + "," + b)
|
||||
.orElse("");
|
||||
MDC.put("userRoles", roles);
|
||||
}
|
||||
|
||||
|
||||
// Store query parameters (without values for privacy)
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
if (parameterMap != null && !parameterMap.isEmpty()) {
|
||||
String params = String.join(",", parameterMap.keySet());
|
||||
MDC.put("queryParams", params);
|
||||
}
|
||||
|
||||
|
||||
// Continue with the filter chain
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
|
||||
} finally {
|
||||
// Clear MDC after request is processed
|
||||
MDC.remove("userAgent");
|
||||
@@ -94,4 +90,4 @@ public class AuditWebFilter extends OncePerRequestFilter {
|
||||
MDC.remove("queryParams");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -1,33 +1,33 @@
|
||||
package stirling.software.proprietary.web;
|
||||
|
||||
import io.github.pixee.security.Newlines;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import io.github.pixee.security.Newlines;
|
||||
|
||||
/**
|
||||
* Guarantees every request carries a stable X-Request-Id; propagates to MDC.
|
||||
*/
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/** Guarantees every request carries a stable X-Request-Id; propagates to MDC. */
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CorrelationIdFilter extends OncePerRequestFilter {
|
||||
|
||||
public static final String HEADER = "X-Request-Id";
|
||||
public static final String MDC_KEY = "requestId";
|
||||
public static final String HEADER = "X-Request-Id";
|
||||
public static final String MDC_KEY = "requestId";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest req,
|
||||
HttpServletResponse res,
|
||||
FilterChain chain)
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest req, HttpServletResponse res, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
try {
|
||||
|
||||
@@ -44,12 +44,12 @@ function setupThemeChangeListener() {
|
||||
|
||||
// Observe the document element for theme changes
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
|
||||
|
||||
// Also observe body for class changes
|
||||
observer.observe(document.body, { attributes: true });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize DOM references
|
||||
auditTableBody = document.getElementById('auditTableBody');
|
||||
pageSizeSelect = document.getElementById('pageSizeSelect');
|
||||
@@ -60,25 +60,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
endDateFilterInput = document.getElementById('endDateFilter');
|
||||
applyFiltersButton = document.getElementById('applyFilters');
|
||||
resetFiltersButton = document.getElementById('resetFilters');
|
||||
|
||||
|
||||
// Load event types for dropdowns
|
||||
loadEventTypes();
|
||||
|
||||
|
||||
// Show a loading message immediately
|
||||
if (auditTableBody) {
|
||||
auditTableBody.innerHTML =
|
||||
auditTableBody.innerHTML =
|
||||
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> ' + window.i18n.loading + '</td></tr>';
|
||||
}
|
||||
|
||||
|
||||
// Make a direct API call first to avoid validation issues
|
||||
loadAuditData(0, pageSize);
|
||||
|
||||
|
||||
// Load statistics for dashboard
|
||||
loadStats(7);
|
||||
|
||||
|
||||
// Setup theme change listener
|
||||
setupThemeChangeListener();
|
||||
|
||||
|
||||
// Set up event listeners
|
||||
pageSizeSelect.addEventListener('change', function() {
|
||||
pageSize = parseInt(this.value);
|
||||
@@ -87,7 +87,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
window.requestedPage = 0;
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
|
||||
applyFiltersButton.addEventListener('click', function() {
|
||||
typeFilter = typeFilterInput.value.trim();
|
||||
principalFilter = principalFilterInput.value.trim();
|
||||
@@ -97,31 +97,31 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
window.requestedPage = 0;
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
|
||||
resetFiltersButton.addEventListener('click', function() {
|
||||
// Reset input fields
|
||||
typeFilterInput.value = '';
|
||||
principalFilterInput.value = '';
|
||||
startDateFilterInput.value = '';
|
||||
endDateFilterInput.value = '';
|
||||
|
||||
|
||||
// Reset filter variables
|
||||
typeFilter = '';
|
||||
principalFilter = '';
|
||||
startDateFilter = '';
|
||||
endDateFilter = '';
|
||||
|
||||
|
||||
// Reset page
|
||||
currentPage = 0;
|
||||
window.requestedPage = 0;
|
||||
|
||||
|
||||
// Update UI
|
||||
document.getElementById('currentPage').textContent = '1';
|
||||
|
||||
// Load data with reset filters
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
|
||||
// Reset export filters button
|
||||
document.getElementById('resetExportFilters').addEventListener('click', function() {
|
||||
exportTypeFilter.value = '';
|
||||
@@ -129,43 +129,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
exportStartDateFilter.value = '';
|
||||
exportEndDateFilter.value = '';
|
||||
});
|
||||
|
||||
|
||||
// Make radio buttons behave like toggle buttons
|
||||
const radioLabels = document.querySelectorAll('label.btn-outline-primary');
|
||||
radioLabels.forEach(label => {
|
||||
const radio = label.querySelector('input[type="radio"]');
|
||||
|
||||
|
||||
if (radio) {
|
||||
// Highlight the checked radio button's label
|
||||
if (radio.checked) {
|
||||
label.classList.add('active');
|
||||
}
|
||||
|
||||
|
||||
// Handle clicking on the label
|
||||
label.addEventListener('click', function() {
|
||||
// Remove active class from all labels
|
||||
radioLabels.forEach(l => l.classList.remove('active'));
|
||||
|
||||
|
||||
// Add active class to this label
|
||||
this.classList.add('active');
|
||||
|
||||
|
||||
// Check this radio button
|
||||
radio.checked = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Handle export button
|
||||
exportButton.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// Get selected format with fallback
|
||||
const selectedRadio = document.querySelector('input[name="exportFormat"]:checked');
|
||||
const exportFormat = selectedRadio ? selectedRadio.value : 'csv';
|
||||
exportAuditData(exportFormat);
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
// Set up pagination buttons
|
||||
document.getElementById('page-first').onclick = function() {
|
||||
if (currentPage > 0) {
|
||||
@@ -173,28 +173,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
document.getElementById('page-prev').onclick = function() {
|
||||
if (currentPage > 0) {
|
||||
goToPage(currentPage - 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
document.getElementById('page-next').onclick = function() {
|
||||
if (currentPage < totalPages - 1) {
|
||||
goToPage(currentPage + 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
document.getElementById('page-last').onclick = function() {
|
||||
if (totalPages > 0 && currentPage < totalPages - 1) {
|
||||
goToPage(totalPages - 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
// Set up tab change events
|
||||
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]');
|
||||
tabEls.forEach(tabEl => {
|
||||
@@ -214,41 +214,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function loadAuditData(targetPage, realPageSize) {
|
||||
const requestedPage = targetPage !== undefined ? targetPage : window.requestedPage || 0;
|
||||
realPageSize = realPageSize || pageSize;
|
||||
|
||||
|
||||
showLoading('table-loading');
|
||||
|
||||
|
||||
// Always request page 0 from server, but with increased page size if needed
|
||||
let url = `/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
||||
|
||||
|
||||
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
|
||||
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
|
||||
if (startDateFilter) url += `&startDate=${startDateFilter}`;
|
||||
if (endDateFilter) url += `&endDate=${endDateFilter}`;
|
||||
|
||||
|
||||
// Update page indicator
|
||||
if (document.getElementById('page-indicator')) {
|
||||
document.getElementById('page-indicator').textContent = `Page ${requestedPage + 1} of ?`;
|
||||
}
|
||||
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
|
||||
|
||||
|
||||
|
||||
// Calculate the correct slice of data to show for the requested page
|
||||
let displayContent = data.content;
|
||||
|
||||
|
||||
// Render the correct slice of data
|
||||
renderTable(displayContent);
|
||||
|
||||
|
||||
// Calculate total pages based on the actual total elements
|
||||
const calculatedTotalPages = Math.ceil(data.totalElements / realPageSize);
|
||||
totalPages = calculatedTotalPages;
|
||||
currentPage = requestedPage; // Use our tracked page, not server's
|
||||
|
||||
|
||||
|
||||
|
||||
// Update UI
|
||||
document.getElementById('currentPage').textContent = currentPage + 1;
|
||||
document.getElementById('totalPages').textContent = totalPages;
|
||||
@@ -256,41 +256,41 @@ function loadAuditData(targetPage, realPageSize) {
|
||||
if (document.getElementById('page-indicator')) {
|
||||
document.getElementById('page-indicator').textContent = `Page ${currentPage + 1} of ${totalPages}`;
|
||||
}
|
||||
|
||||
|
||||
// Re-enable buttons with correct state
|
||||
document.getElementById('page-first').disabled = currentPage === 0;
|
||||
document.getElementById('page-prev').disabled = currentPage === 0;
|
||||
document.getElementById('page-next').disabled = currentPage >= totalPages - 1;
|
||||
document.getElementById('page-last').disabled = currentPage >= totalPages - 1;
|
||||
|
||||
|
||||
hideLoading('table-loading');
|
||||
|
||||
|
||||
// Restore original page size for next operations
|
||||
if (window.originalPageSize && realPageSize !== window.originalPageSize) {
|
||||
pageSize = window.originalPageSize;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Store original page size for recovery
|
||||
window.originalPageSize = realPageSize;
|
||||
|
||||
|
||||
// Clear busy flag
|
||||
window.paginationBusy = false;
|
||||
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
|
||||
if (auditTableBody) {
|
||||
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">${window.i18n.errorLoading} ${error.message}</td></tr>`;
|
||||
}
|
||||
hideLoading('table-loading');
|
||||
|
||||
|
||||
// Re-enable buttons
|
||||
document.getElementById('page-first').disabled = false;
|
||||
document.getElementById('page-prev').disabled = false;
|
||||
document.getElementById('page-next').disabled = false;
|
||||
document.getElementById('page-last').disabled = false;
|
||||
|
||||
|
||||
// Clear busy flag
|
||||
window.paginationBusy = false;
|
||||
});
|
||||
@@ -301,7 +301,7 @@ function loadStats(days) {
|
||||
showLoading('type-chart-loading');
|
||||
showLoading('user-chart-loading');
|
||||
showLoading('time-chart-loading');
|
||||
|
||||
|
||||
fetch(`/audit/stats?days=${days}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
@@ -327,29 +327,29 @@ function exportAuditData(format) {
|
||||
const principal = exportPrincipalFilter.value.trim();
|
||||
const startDate = exportStartDateFilter.value;
|
||||
const endDate = exportEndDateFilter.value;
|
||||
|
||||
|
||||
let url = format === 'json' ? '/audit/export/json?' : '/audit/export?';
|
||||
|
||||
|
||||
if (type) url += `&type=${encodeURIComponent(type)}`;
|
||||
if (principal) url += `&principal=${encodeURIComponent(principal)}`;
|
||||
if (startDate) url += `&startDate=${startDate}`;
|
||||
if (endDate) url += `&endDate=${endDate}`;
|
||||
|
||||
|
||||
// Trigger download
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Render table with audit data
|
||||
function renderTable(events) {
|
||||
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.noEventsFound + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
auditTableBody.innerHTML = '';
|
||||
|
||||
|
||||
events.forEach((event, index) => {
|
||||
try {
|
||||
const row = document.createElement('tr');
|
||||
@@ -360,10 +360,10 @@ function renderTable(events) {
|
||||
<td>${escapeHtml(event.type || 'N/A')}</td>
|
||||
<td><button class="btn btn-sm btn-outline-primary view-details">${window.i18n.viewDetails || 'View Details'}</button></td>
|
||||
`;
|
||||
|
||||
|
||||
// Store event data for modal
|
||||
row.dataset.event = JSON.stringify(event);
|
||||
|
||||
|
||||
// Add click handler for details button
|
||||
const detailsButton = row.querySelector('.view-details');
|
||||
if (detailsButton) {
|
||||
@@ -371,13 +371,13 @@ function renderTable(events) {
|
||||
showEventDetails(event);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
auditTableBody.appendChild(row);
|
||||
} catch (rowError) {
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} catch (e) {
|
||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">' + window.i18n.errorRendering + ' ' + e.message + '</td></tr>';
|
||||
}
|
||||
@@ -392,13 +392,13 @@ function showEventDetails(event) {
|
||||
const modalTimestamp = document.getElementById('modal-timestamp');
|
||||
const modalData = document.getElementById('modal-data');
|
||||
const eventDetailsModal = document.getElementById('eventDetailsModal');
|
||||
|
||||
|
||||
// Set modal content
|
||||
if (modalId) modalId.textContent = event.id;
|
||||
if (modalPrincipal) modalPrincipal.textContent = event.principal;
|
||||
if (modalType) modalType.textContent = event.type;
|
||||
if (modalTimestamp) modalTimestamp.textContent = formatDate(event.timestamp);
|
||||
|
||||
|
||||
// Format JSON data
|
||||
if (modalData) {
|
||||
try {
|
||||
@@ -408,7 +408,7 @@ function showEventDetails(event) {
|
||||
modalData.textContent = event.data || 'No data available';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show the modal
|
||||
if (eventDetailsModal) {
|
||||
const modal = new bootstrap.Modal(eventDetailsModal);
|
||||
@@ -420,32 +420,32 @@ function showEventDetails(event) {
|
||||
|
||||
// Direct pagination approach - server seems to be hard-limited to returning 20 items
|
||||
function goToPage(page) {
|
||||
|
||||
|
||||
// Basic validation - totalPages may not be initialized on first load
|
||||
if (page < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Skip validation against totalPages on first load
|
||||
if (totalPages > 0 && page >= totalPages) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Simple guard flag
|
||||
if (window.paginationBusy) {
|
||||
return;
|
||||
}
|
||||
window.paginationBusy = true;
|
||||
|
||||
|
||||
try {
|
||||
|
||||
// Store the requested page for later
|
||||
window.requestedPage = page;
|
||||
currentPage = page;
|
||||
|
||||
|
||||
// Update UI immediately for user feedback
|
||||
document.getElementById('currentPage').textContent = page + 1;
|
||||
|
||||
|
||||
// Load data with this page
|
||||
loadAuditData(page, pageSize);
|
||||
} catch (e) {
|
||||
@@ -457,27 +457,27 @@ function goToPage(page) {
|
||||
function renderCharts(data) {
|
||||
// Get theme colors
|
||||
const colors = getThemeColors();
|
||||
|
||||
|
||||
// Prepare data for charts
|
||||
const typeLabels = Object.keys(data.eventsByType);
|
||||
const typeValues = Object.values(data.eventsByType);
|
||||
|
||||
|
||||
const userLabels = Object.keys(data.eventsByPrincipal);
|
||||
const userValues = Object.values(data.eventsByPrincipal);
|
||||
|
||||
|
||||
// Sort days for time chart
|
||||
const timeLabels = Object.keys(data.eventsByDay).sort();
|
||||
const timeValues = timeLabels.map(day => data.eventsByDay[day] || 0);
|
||||
|
||||
|
||||
// Chart.js global defaults for dark mode compatibility
|
||||
Chart.defaults.color = colors.text;
|
||||
Chart.defaults.borderColor = colors.grid;
|
||||
|
||||
|
||||
// Type chart
|
||||
if (typeChart) {
|
||||
typeChart.destroy();
|
||||
}
|
||||
|
||||
|
||||
const typeCtx = document.getElementById('typeChart').getContext('2d');
|
||||
typeChart = new Chart(typeCtx, {
|
||||
type: 'bar',
|
||||
@@ -600,12 +600,12 @@ function renderCharts(data) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// User chart
|
||||
if (userChart) {
|
||||
userChart.destroy();
|
||||
}
|
||||
|
||||
|
||||
const userCtx = document.getElementById('userChart').getContext('2d');
|
||||
userChart = new Chart(userCtx, {
|
||||
type: 'pie',
|
||||
@@ -640,7 +640,7 @@ function renderCharts(data) {
|
||||
generateLabels: function(chart) {
|
||||
const original = Chart.overrides.pie.plugins.legend.labels.generateLabels;
|
||||
const labels = original.call(this, chart);
|
||||
|
||||
|
||||
if (colors.isDarkMode) {
|
||||
labels.forEach(label => {
|
||||
// Enhance contrast for dark mode
|
||||
@@ -649,7 +649,7 @@ function renderCharts(data) {
|
||||
label.lineWidth = 2; // Thicker border
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
@@ -673,14 +673,14 @@ function renderCharts(data) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Time chart
|
||||
if (timeChart) {
|
||||
timeChart.destroy();
|
||||
}
|
||||
|
||||
|
||||
const timeCtx = document.getElementById('timeChart').getContext('2d');
|
||||
|
||||
|
||||
// Get first color for line chart with appropriate transparency
|
||||
let bgColor, borderColor;
|
||||
if (colors.isDarkMode) {
|
||||
@@ -690,7 +690,7 @@ function renderCharts(data) {
|
||||
bgColor = 'rgba(0, 96, 170, 0.2)'; // Dark blue with transparency
|
||||
borderColor = 'rgb(0, 96, 170)'; // Dark blue solid
|
||||
}
|
||||
|
||||
|
||||
timeChart = new Chart(timeCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
@@ -841,20 +841,20 @@ function loadEventTypes() {
|
||||
if (!types || types.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Populate the type filter dropdowns
|
||||
const typeFilter = document.getElementById('typeFilter');
|
||||
const exportTypeFilter = document.getElementById('exportTypeFilter');
|
||||
|
||||
|
||||
// Clear existing options except the first one (All event types)
|
||||
while (typeFilter.options.length > 1) {
|
||||
typeFilter.remove(1);
|
||||
}
|
||||
|
||||
|
||||
while (exportTypeFilter.options.length > 1) {
|
||||
exportTypeFilter.remove(1);
|
||||
}
|
||||
|
||||
|
||||
// Add new options
|
||||
types.forEach(type => {
|
||||
// Main filter dropdown
|
||||
@@ -862,7 +862,7 @@ function loadEventTypes() {
|
||||
option.value = type;
|
||||
option.textContent = type;
|
||||
typeFilter.appendChild(option);
|
||||
|
||||
|
||||
// Export filter dropdown
|
||||
const exportOption = document.createElement('option');
|
||||
exportOption.value = type;
|
||||
@@ -878,17 +878,17 @@ function loadEventTypes() {
|
||||
// Get theme colors for charts
|
||||
function getThemeColors() {
|
||||
const isDarkMode = document.documentElement.getAttribute('data-bs-theme') === 'dark';
|
||||
|
||||
|
||||
// In dark mode, use higher contrast colors for text
|
||||
const textColor = isDarkMode ?
|
||||
const textColor = isDarkMode ?
|
||||
'rgb(255, 255, 255)' : // White for dark mode for maximum contrast
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-on-surface').trim();
|
||||
|
||||
|
||||
// Use a more visible grid color in dark mode
|
||||
const gridColor = isDarkMode ?
|
||||
'rgba(255, 255, 255, 0.2)' : // Semi-transparent white for dark mode
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-outline-variant').trim();
|
||||
|
||||
|
||||
// Define bright, high-contrast colors for both dark and light modes
|
||||
const chartColorsDark = [
|
||||
'rgb(162, 201, 255)', // Light blue - primary
|
||||
@@ -902,7 +902,7 @@ function getThemeColors() {
|
||||
'rgb(212, 172, 25)', // Yellow - image
|
||||
'rgb(245, 84, 84)', // Red - advance
|
||||
];
|
||||
|
||||
|
||||
const chartColorsLight = [
|
||||
'rgb(0, 96, 170)', // Blue - primary
|
||||
'rgb(88, 90, 138)', // Purple - tertiary
|
||||
@@ -915,7 +915,7 @@ function getThemeColors() {
|
||||
'rgb(212, 172, 25)', // Yellow - image
|
||||
'rgb(245, 84, 84)', // Red - advance
|
||||
];
|
||||
|
||||
|
||||
return {
|
||||
text: textColor,
|
||||
grid: gridColor,
|
||||
@@ -935,7 +935,7 @@ function getChartColors(count, opacity = 0.6) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Get the raw color and add opacity
|
||||
let color = themeColors.chartColors[i % themeColors.chartColors.length];
|
||||
// If it's rgb() format, convert to rgba()
|
||||
// If it's rgb() format, convert to rgba()
|
||||
if (color.startsWith('rgb(')) {
|
||||
color = color.replace('rgb(', '').replace(')', '');
|
||||
result.push(`rgba(${color}, ${opacity})`);
|
||||
@@ -949,7 +949,7 @@ function getChartColors(count, opacity = 0.6) {
|
||||
} catch (e) {
|
||||
console.warn('Error using theme colors, falling back to default colors', e);
|
||||
}
|
||||
|
||||
|
||||
// Base colors - a larger palette than the default
|
||||
const colors = [
|
||||
[54, 162, 235], // blue
|
||||
@@ -973,9 +973,9 @@ function getChartColors(count, opacity = 0.6) {
|
||||
[41, 128, 185], // belize hole
|
||||
[142, 68, 173] // wisteria
|
||||
];
|
||||
|
||||
|
||||
const result = [];
|
||||
|
||||
|
||||
// Always use the same format regardless of color source
|
||||
if (count > colors.length) {
|
||||
// Generate colors algorithmically for large sets
|
||||
@@ -984,7 +984,7 @@ function getChartColors(count, opacity = 0.6) {
|
||||
const hue = (i * 360 / count) % 360;
|
||||
const sat = 70 + Math.random() * 10; // 70-80%
|
||||
const light = 50 + Math.random() * 10; // 50-60%
|
||||
|
||||
|
||||
result.push(`hsla(${hue}, ${sat}%, ${light}%, ${opacity})`);
|
||||
}
|
||||
} else {
|
||||
@@ -994,6 +994,6 @@ function getChartColors(count, opacity = 0.6) {
|
||||
result.push(`rgba(${color[0]}, ${color[1]}, ${color[2]}, ${opacity})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user