feat: add audit domain model, repository interface, AuditService, and unit test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.cameleer3.server.app.admin;
|
||||
|
||||
import com.cameleer3.server.core.admin.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class AuditServiceTest {
|
||||
private AuditRepository mockRepository;
|
||||
private AuditService auditService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockRepository = mock(AuditRepository.class);
|
||||
auditService = new AuditService(mockRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void log_withExplicitUsername_insertsRecordWithCorrectFields() {
|
||||
var request = mock(HttpServletRequest.class);
|
||||
when(request.getRemoteAddr()).thenReturn("192.168.1.1");
|
||||
when(request.getHeader("User-Agent")).thenReturn("Mozilla/5.0");
|
||||
|
||||
auditService.log("admin", "kill_query", AuditCategory.INFRA, "PID 42",
|
||||
Map.of("query", "SELECT 1"), AuditResult.SUCCESS, request);
|
||||
|
||||
var captor = ArgumentCaptor.forClass(AuditRecord.class);
|
||||
verify(mockRepository).insert(captor.capture());
|
||||
var record = captor.getValue();
|
||||
assertEquals("admin", record.username());
|
||||
assertEquals("kill_query", record.action());
|
||||
assertEquals(AuditCategory.INFRA, record.category());
|
||||
assertEquals("PID 42", record.target());
|
||||
assertEquals("192.168.1.1", record.ipAddress());
|
||||
assertEquals("Mozilla/5.0", record.userAgent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void log_withNullRequest_handlesGracefully() {
|
||||
auditService.log("admin", "test", AuditCategory.CONFIG, null, null, AuditResult.SUCCESS, null);
|
||||
verify(mockRepository).insert(any(AuditRecord.class));
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,16 @@
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.cameleer3.server.core.admin;
|
||||
|
||||
public enum AuditCategory {
|
||||
INFRA, AUTH, USER_MGMT, CONFIG
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.cameleer3.server.core.admin;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
public record AuditRecord(
|
||||
long id,
|
||||
Instant timestamp,
|
||||
String username,
|
||||
String action,
|
||||
AuditCategory category,
|
||||
String target,
|
||||
Map<String, Object> detail,
|
||||
AuditResult result,
|
||||
String ipAddress,
|
||||
String userAgent
|
||||
) {
|
||||
/** Factory for creating new records (id and timestamp assigned by DB) */
|
||||
public static AuditRecord create(String username, String action, AuditCategory category,
|
||||
String target, Map<String, Object> detail, AuditResult result,
|
||||
String ipAddress, String userAgent) {
|
||||
return new AuditRecord(0, null, username, action, category, target, detail, result, ipAddress, userAgent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.cameleer3.server.core.admin;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public interface AuditRepository {
|
||||
|
||||
void insert(AuditRecord record);
|
||||
|
||||
record AuditQuery(
|
||||
String username,
|
||||
AuditCategory category,
|
||||
String search,
|
||||
Instant from,
|
||||
Instant to,
|
||||
String sort,
|
||||
String order,
|
||||
int page,
|
||||
int size
|
||||
) {}
|
||||
|
||||
record AuditPage(List<AuditRecord> items, long totalCount) {}
|
||||
|
||||
AuditPage find(AuditQuery query);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.cameleer3.server.core.admin;
|
||||
|
||||
public enum AuditResult {
|
||||
SUCCESS, FAILURE
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.cameleer3.server.core.admin;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class AuditService {
|
||||
private static final Logger log = LoggerFactory.getLogger(AuditService.class);
|
||||
private final AuditRepository repository;
|
||||
|
||||
public AuditService(AuditRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
/** Log an action using the current SecurityContext for username */
|
||||
public void log(String action, AuditCategory category, String target,
|
||||
Map<String, Object> detail, AuditResult result,
|
||||
HttpServletRequest request) {
|
||||
String username = extractUsername();
|
||||
log(username, action, category, target, detail, result, request);
|
||||
}
|
||||
|
||||
/** Log an action with explicit username (for pre-auth contexts like login) */
|
||||
public void log(String username, String action, AuditCategory category, String target,
|
||||
Map<String, Object> detail, AuditResult result,
|
||||
HttpServletRequest request) {
|
||||
String ip = request != null ? request.getRemoteAddr() : null;
|
||||
String userAgent = request != null ? request.getHeader("User-Agent") : null;
|
||||
AuditRecord record = AuditRecord.create(username, action, category, target, detail, result, ip, userAgent);
|
||||
|
||||
repository.insert(record);
|
||||
|
||||
log.info("AUDIT: user={} action={} category={} target={} result={}",
|
||||
username, action, category, target, result);
|
||||
}
|
||||
|
||||
private String extractUsername() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.getName() != null) {
|
||||
String name = auth.getName();
|
||||
return name.startsWith("user:") ? name.substring(5) : name;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user