mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c99674f430 |
@@ -699,6 +699,7 @@ public class ApplicationProperties {
|
||||
private boolean enabled = true;
|
||||
private int level = 2; // 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE
|
||||
private int retentionDays = 90;
|
||||
private boolean logIpAddresses = false; // Privacy: IP logging disabled by default
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -97,6 +97,7 @@ premium:
|
||||
enabled: true # Enable audit logging
|
||||
level: 2 # Audit logging level: 0=OFF, 1=BASIC, 2=STANDARD, 3=VERBOSE
|
||||
retentionDays: 90 # Number of days to retain audit logs
|
||||
logIpAddresses: false # Log client IP addresses (privacy-sensitive)
|
||||
databaseNotifications:
|
||||
backups:
|
||||
successful: false # set to 'true' to enable email notifications for successful database backups
|
||||
|
||||
+82
-21
@@ -49,13 +49,38 @@ public class AuditAspect {
|
||||
Map<String, Object> auditData =
|
||||
AuditUtils.createBaseAuditData(joinPoint, auditedAnnotation.level());
|
||||
|
||||
// Add HTTP information if we're in a web context
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs != null) {
|
||||
HttpServletRequest req = attrs.getRequest();
|
||||
String path = req.getRequestURI();
|
||||
String httpMethod = req.getMethod();
|
||||
// Try to find HttpServletRequest from method arguments first (for Security handlers)
|
||||
HttpServletRequest request = null;
|
||||
for (Object arg : joinPoint.getArgs()) {
|
||||
if (arg instanceof HttpServletRequest) {
|
||||
request = (HttpServletRequest) arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to RequestContextHolder if not in method args
|
||||
if (request == null) {
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs != null) {
|
||||
request = attrs.getRequest();
|
||||
}
|
||||
}
|
||||
|
||||
// Capture principal, origin, and IP early (before any async execution)
|
||||
// Extract IP directly from the request we already have (more reliable than
|
||||
// RequestContextHolder)
|
||||
String capturedPrincipal = auditService.captureCurrentPrincipal();
|
||||
String capturedOrigin = auditService.captureCurrentOrigin();
|
||||
String capturedIp = null;
|
||||
if (request != null && auditConfig.isLogIpAddresses()) {
|
||||
capturedIp = AuditUtils.extractClientIp(request);
|
||||
}
|
||||
|
||||
// Add HTTP information if we have a valid request
|
||||
if (request != null) {
|
||||
String path = request.getRequestURI();
|
||||
String httpMethod = request.getMethod();
|
||||
AuditUtils.addHttpData(auditData, httpMethod, path, auditedAnnotation.level());
|
||||
AuditUtils.addFileData(auditData, joinPoint, auditedAnnotation.level());
|
||||
}
|
||||
@@ -101,20 +126,43 @@ public class AuditAspect {
|
||||
// Re-throw the exception
|
||||
throw ex;
|
||||
} finally {
|
||||
// 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);
|
||||
// Find HttpServletResponse from method arguments first
|
||||
HttpServletResponse response = null;
|
||||
for (Object arg : joinPoint.getArgs()) {
|
||||
if (arg instanceof HttpServletResponse) {
|
||||
response = (HttpServletResponse) arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to RequestContextHolder for response (most controllers don't have it in
|
||||
// args)
|
||||
if (response == null) {
|
||||
ServletRequestAttributes attrs =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs != null) {
|
||||
response = attrs.getResponse();
|
||||
}
|
||||
}
|
||||
|
||||
// Add timing directly (like ControllerAuditAspect) when we have a request
|
||||
if (auditedAnnotation.level().includes(AuditLevel.STANDARD)) {
|
||||
auditData.put("latencyMs", System.currentTimeMillis() - startTime);
|
||||
if (response != null) {
|
||||
try {
|
||||
auditData.put("statusCode", response.getStatus());
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the event type based on annotation and context
|
||||
String httpMethod = null;
|
||||
String path = null;
|
||||
if (attrs != null) {
|
||||
HttpServletRequest req = attrs.getRequest();
|
||||
httpMethod = req.getMethod();
|
||||
path = req.getRequestURI();
|
||||
if (request != null) {
|
||||
httpMethod = request.getMethod();
|
||||
path = request.getRequestURI();
|
||||
}
|
||||
|
||||
AuditEventType eventType =
|
||||
@@ -128,11 +176,24 @@ public class AuditAspect {
|
||||
// Check if we should use string type instead
|
||||
String typeString = auditedAnnotation.typeString();
|
||||
if (eventType == AuditEventType.HTTP_REQUEST && StringUtils.isNotEmpty(typeString)) {
|
||||
// Use the string type (for backward compatibility)
|
||||
auditService.audit(typeString, auditData, auditedAnnotation.level());
|
||||
// Use the string type (for backward compatibility) with captured principal, origin,
|
||||
// and IP
|
||||
auditService.audit(
|
||||
capturedPrincipal,
|
||||
capturedOrigin,
|
||||
capturedIp,
|
||||
typeString,
|
||||
auditData,
|
||||
auditedAnnotation.level());
|
||||
} else {
|
||||
// Use the enum type (preferred)
|
||||
auditService.audit(eventType, auditData, auditedAnnotation.level());
|
||||
// Use the enum type (preferred) with captured principal, origin, and IP
|
||||
auditService.audit(
|
||||
capturedPrincipal,
|
||||
capturedOrigin,
|
||||
capturedIp,
|
||||
eventType,
|
||||
auditData,
|
||||
auditedAnnotation.level());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,17 +48,6 @@ public class AuditUtils {
|
||||
ProceedingJoinPoint joinPoint, AuditLevel auditLevel) {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
// Common data for all levels
|
||||
data.put("timestamp", Instant.now().toString());
|
||||
|
||||
// Add principal if available
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.getName() != null) {
|
||||
data.put("principal", auth.getName());
|
||||
} else {
|
||||
data.put("principal", "system");
|
||||
}
|
||||
|
||||
// Add class name and method name only at VERBOSE level
|
||||
if (auditLevel.includes(AuditLevel.VERBOSE)) {
|
||||
data.put("className", joinPoint.getTarget().getClass().getName());
|
||||
@@ -102,7 +91,6 @@ public class AuditUtils {
|
||||
|
||||
// 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);
|
||||
@@ -424,4 +412,38 @@ public class AuditUtils {
|
||||
&& !RequestUriUtils.isTrackableResource(
|
||||
request.getContextPath(), request.getRequestURI());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract client IP address from HTTP request, preferring forwarded headers for use behind
|
||||
* proxies/load balancers. Truncates to 45 characters to fit database column constraints.
|
||||
*
|
||||
* @param request The HTTP request (may be null)
|
||||
* @return The client IP address, or null if not available
|
||||
*/
|
||||
public static String extractClientIp(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prefer X-Forwarded-For header (used by most proxies/load balancers)
|
||||
String xff = request.getHeader("X-Forwarded-For");
|
||||
if (xff != null && !xff.isBlank()) {
|
||||
// X-Forwarded-For can be a comma-separated list: "client, proxy1, proxy2"
|
||||
// Take the first (client) IP
|
||||
String first = xff.split(",")[0].trim();
|
||||
return first.length() > 45 ? first.substring(0, 45) : first;
|
||||
}
|
||||
|
||||
// Try X-Real-IP header (used by some reverse proxies like nginx)
|
||||
String realIp = request.getHeader("X-Real-IP");
|
||||
if (realIp != null && !realIp.isBlank()) {
|
||||
return realIp.length() > 45 ? realIp.substring(0, 45) : realIp;
|
||||
}
|
||||
|
||||
// Fall back to remote address
|
||||
String remoteAddr = request.getRemoteAddr();
|
||||
return remoteAddr != null && remoteAddr.length() > 45
|
||||
? remoteAddr.substring(0, 45)
|
||||
: remoteAddr;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-4
@@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.service.AuditService;
|
||||
|
||||
@@ -37,7 +38,7 @@ import stirling.software.proprietary.service.AuditService;
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@org.springframework.core.annotation.Order(
|
||||
10) // Lower precedence (higher number) - executes after AutoJobAspect
|
||||
0) // Highest precedence - runs on request thread before AutoJobAspect
|
||||
public class ControllerAuditAspect {
|
||||
|
||||
private final AuditService auditService;
|
||||
@@ -121,6 +122,16 @@ public class ControllerAuditAspect {
|
||||
HttpServletRequest req = attrs != null ? attrs.getRequest() : null;
|
||||
HttpServletResponse resp = attrs != null ? attrs.getResponse() : null;
|
||||
|
||||
// Capture principal, origin, and IP (runs on request thread before AutoJobAspect)
|
||||
String capturedPrincipal = auditService.captureCurrentPrincipal();
|
||||
String capturedOrigin = auditService.captureCurrentOrigin();
|
||||
// Extract IP directly from the request we already have (more reliable than
|
||||
// RequestContextHolder)
|
||||
String capturedIp = null;
|
||||
if (req != null && auditConfig.isLogIpAddresses()) {
|
||||
capturedIp = AuditUtils.extractClientIp(req);
|
||||
}
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
// Use AuditUtils to create the base audit data
|
||||
@@ -176,13 +187,15 @@ public class ControllerAuditAspect {
|
||||
String typeString = auditedAnnotation.typeString();
|
||||
if (eventType == AuditEventType.HTTP_REQUEST
|
||||
&& StringUtils.isNotEmpty(typeString)) {
|
||||
auditService.audit(typeString, data, level);
|
||||
auditService.audit(
|
||||
capturedPrincipal, capturedOrigin, capturedIp, typeString, data, level);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the enum type
|
||||
auditService.audit(eventType, data, level);
|
||||
// Use the enum type with captured principal, origin, and IP
|
||||
auditService.audit(
|
||||
capturedPrincipal, capturedOrigin, capturedIp, eventType, data, level);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -194,6 +207,14 @@ public class ControllerAuditAspect {
|
||||
RequestMapping cm = method.getDeclaringClass().getAnnotation(RequestMapping.class);
|
||||
if (cm != null && cm.value().length > 0) base = cm.value()[0];
|
||||
String mp = "";
|
||||
|
||||
// First check for AutoJobPostMapping (which is also a POST)
|
||||
AutoJobPostMapping autoJob = method.getAnnotation(AutoJobPostMapping.class);
|
||||
if (autoJob != null && autoJob.value().length > 0) {
|
||||
return base + autoJob.value()[0];
|
||||
}
|
||||
|
||||
// Fall back to standard mappings
|
||||
Annotation ann =
|
||||
switch (httpMethod) {
|
||||
case "GET" -> method.getAnnotation(GetMapping.class);
|
||||
|
||||
+7
-2
@@ -23,6 +23,7 @@ public class AuditConfigurationProperties {
|
||||
private final boolean enabled;
|
||||
private final int level;
|
||||
private final int retentionDays;
|
||||
private final boolean logIpAddresses;
|
||||
|
||||
public AuditConfigurationProperties(ApplicationProperties applicationProperties) {
|
||||
ApplicationProperties.Premium.EnterpriseFeatures.Audit auditConfig =
|
||||
@@ -37,11 +38,15 @@ public class AuditConfigurationProperties {
|
||||
// Retention days (0 means infinite)
|
||||
this.retentionDays = auditConfig.getRetentionDays();
|
||||
|
||||
// IP address logging (default false for privacy)
|
||||
this.logIpAddresses = auditConfig.isLogIpAddresses();
|
||||
|
||||
log.debug(
|
||||
"Initialized audit configuration: enabled={}, level={}, retentionDays={} (0=infinite)",
|
||||
"Initialized audit configuration: enabled={}, level={}, retentionDays={} (0=infinite), logIpAddresses={}",
|
||||
this.enabled,
|
||||
this.level,
|
||||
this.retentionDays);
|
||||
this.retentionDays,
|
||||
this.logIpAddresses);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+64
-1
@@ -9,6 +9,8 @@ import org.springframework.boot.actuate.audit.AuditEvent;
|
||||
import org.springframework.boot.actuate.audit.AuditEventRepository;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -49,6 +51,23 @@ public class CustomAuditEventRepository implements AuditEventRepository {
|
||||
if (clean.isEmpty() || (clean.size() == 1 && clean.containsKey("details"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract origin and IP from the data map (set by AuditService before async call)
|
||||
String origin = "SYSTEM"; // default
|
||||
String ipAddress = null;
|
||||
|
||||
if (clean.containsKey("__origin") || clean.containsKey("__ipAddress")) {
|
||||
clean = new java.util.HashMap<>(clean);
|
||||
|
||||
if (clean.containsKey("__origin")) {
|
||||
origin = String.valueOf(clean.remove("__origin"));
|
||||
}
|
||||
|
||||
if (clean.containsKey("__ipAddress")) {
|
||||
ipAddress = String.valueOf(clean.remove("__ipAddress"));
|
||||
}
|
||||
}
|
||||
|
||||
String rid = MDC.get("requestId");
|
||||
|
||||
if (rid != null) {
|
||||
@@ -59,16 +78,60 @@ public class CustomAuditEventRepository implements AuditEventRepository {
|
||||
String auditEventData = mapper.writeValueAsString(clean);
|
||||
log.debug("AuditEvent data (JSON): {}", auditEventData);
|
||||
|
||||
String principalName = extractPrincipalName(ev.getPrincipal());
|
||||
|
||||
if (principalName.length() > 255) {
|
||||
log.warn(
|
||||
"Principal length {} exceeds 255 characters, truncating: {}",
|
||||
principalName.length(),
|
||||
principalName.substring(0, Math.min(50, principalName.length())) + "...");
|
||||
principalName = principalName.substring(0, 255);
|
||||
}
|
||||
if (ev.getType().length() > 255) {
|
||||
log.warn(
|
||||
"Type length {} exceeds 255 characters: {}",
|
||||
ev.getType().length(),
|
||||
ev.getType());
|
||||
}
|
||||
|
||||
PersistentAuditEvent ent =
|
||||
PersistentAuditEvent.builder()
|
||||
.principal(ev.getPrincipal())
|
||||
.principal(principalName)
|
||||
.type(ev.getType())
|
||||
.data(auditEventData)
|
||||
.timestamp(ev.getTimestamp())
|
||||
.ipAddress(ipAddress)
|
||||
.origin(origin)
|
||||
.build();
|
||||
repo.save(ent);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // fail-open
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a meaningful principal name from the audit event principal. Uses lightweight
|
||||
* approach to avoid performance impact on high-volume audit events.
|
||||
*/
|
||||
private String extractPrincipalName(String principal) {
|
||||
if (principal == null || principal.isEmpty()) {
|
||||
return "anonymous";
|
||||
}
|
||||
|
||||
// Quick check for JWT tokens (they start with "eyJ")
|
||||
if (principal.startsWith("eyJ")) {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.isAuthenticated()) {
|
||||
String name = auth.getName();
|
||||
if (name != null && !name.startsWith("eyJ")) {
|
||||
return name.length() > 255 ? name.substring(0, 255) : name;
|
||||
}
|
||||
}
|
||||
|
||||
// Unverified/failed JWT; don't trust token content
|
||||
return "authentication-failure";
|
||||
}
|
||||
|
||||
return principal.length() > 255 ? principal.substring(0, 255) : principal;
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -13,6 +13,7 @@ import lombok.*;
|
||||
@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_origin", columnList = "origin"),
|
||||
@jakarta.persistence.Index(
|
||||
name = "idx_audit_principal_type",
|
||||
columnList = "principal,type"),
|
||||
@@ -33,7 +34,14 @@ public class PersistentAuditEvent {
|
||||
private String principal;
|
||||
private String type;
|
||||
|
||||
@Lob private String data; // JSON blob
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String data; // JSON blob
|
||||
|
||||
private Instant timestamp;
|
||||
|
||||
@Column(length = 45) // Max length for IPv6 (xxx:xxx:xxx:xxx:xxx:xxx:xxx:xxx)
|
||||
private String ipAddress;
|
||||
|
||||
@Column(length = 10) // "WEB", "API", or "SYSTEM"
|
||||
private String origin;
|
||||
}
|
||||
|
||||
+110
-2
@@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.security.model.ApiKeyAuthenticationToken;
|
||||
|
||||
/**
|
||||
* Service for creating manual audit events throughout the application. This provides easy access to
|
||||
@@ -53,7 +54,12 @@ public class AuditService {
|
||||
}
|
||||
|
||||
String principal = getCurrentUsername();
|
||||
repository.add(new AuditEvent(principal, type.name(), data));
|
||||
|
||||
// Add origin to the data map (captured here before async execution)
|
||||
Map<String, Object> enrichedData = new java.util.HashMap<>(data);
|
||||
enrichedData.put("__origin", determineOrigin());
|
||||
|
||||
repository.add(new AuditEvent(principal, type.name(), enrichedData));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +121,12 @@ public class AuditService {
|
||||
}
|
||||
|
||||
String principal = getCurrentUsername();
|
||||
repository.add(new AuditEvent(principal, type, data));
|
||||
|
||||
// Add origin to the data map (captured here before async execution)
|
||||
Map<String, Object> enrichedData = new java.util.HashMap<>(data);
|
||||
enrichedData.put("__origin", determineOrigin());
|
||||
|
||||
repository.add(new AuditEvent(principal, type, enrichedData));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,9 +172,106 @@ public class AuditService {
|
||||
audit(principal, type, data, AuditLevel.STANDARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event with pre-captured principal, origin, and IP (for use by audit aspects).
|
||||
*/
|
||||
public void audit(
|
||||
String principal,
|
||||
String origin,
|
||||
String ipAddress,
|
||||
AuditEventType type,
|
||||
Map<String, Object> data,
|
||||
AuditLevel level) {
|
||||
if (!auditConfig.isEnabled()
|
||||
|| !auditConfig.getAuditLevel().includes(level)
|
||||
|| !runningEE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add origin and IP to the data map (already captured before async)
|
||||
Map<String, Object> enrichedData = new java.util.HashMap<>(data);
|
||||
enrichedData.put("__origin", origin);
|
||||
if (ipAddress != null) {
|
||||
enrichedData.put("__ipAddress", ipAddress);
|
||||
}
|
||||
|
||||
repository.add(new AuditEvent(principal, type.name(), enrichedData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an audit event with pre-captured principal, origin, and IP using string type (for
|
||||
* backward compatibility).
|
||||
*/
|
||||
public void audit(
|
||||
String principal,
|
||||
String origin,
|
||||
String ipAddress,
|
||||
String type,
|
||||
Map<String, Object> data,
|
||||
AuditLevel level) {
|
||||
if (!auditConfig.isEnabled()
|
||||
|| !auditConfig.getAuditLevel().includes(level)
|
||||
|| !runningEE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add origin and IP to the data map (already captured before async)
|
||||
Map<String, Object> enrichedData = new java.util.HashMap<>(data);
|
||||
enrichedData.put("__origin", origin);
|
||||
if (ipAddress != null) {
|
||||
enrichedData.put("__ipAddress", ipAddress);
|
||||
}
|
||||
|
||||
repository.add(new AuditEvent(principal, type, enrichedData));
|
||||
}
|
||||
|
||||
/** 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";
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the current principal for later use (avoids SecurityContext issues in async/thread
|
||||
* changes). Public so audit aspects can capture early before thread context changes.
|
||||
*/
|
||||
public String captureCurrentPrincipal() {
|
||||
return getCurrentUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the current origin for later use (avoids SecurityContext issues in async/thread
|
||||
* changes). Public so audit aspects can capture early before thread context changes.
|
||||
*/
|
||||
public String captureCurrentOrigin() {
|
||||
return determineOrigin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the origin of the request: API (X-API-KEY), WEB (JWT), or SYSTEM (no auth).
|
||||
* IMPORTANT: This must be called in the request thread before async execution.
|
||||
*
|
||||
* @return "API", "WEB", or "SYSTEM"
|
||||
*/
|
||||
private String determineOrigin() {
|
||||
try {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
// Check if authenticated via API key
|
||||
if (auth instanceof ApiKeyAuthenticationToken) {
|
||||
return "API";
|
||||
}
|
||||
|
||||
// Check if authenticated via JWT (web user)
|
||||
if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getName())) {
|
||||
return "WEB";
|
||||
}
|
||||
|
||||
// System or unauthenticated
|
||||
return "SYSTEM";
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not determine origin for audit event", e);
|
||||
return "SYSTEM";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user