diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/WebConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/WebConfig.java
index d4af1159..a5132e69 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/WebConfig.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/WebConfig.java
@@ -1,5 +1,6 @@
package com.cameleer3.server.app.config;
+import com.cameleer3.server.app.interceptor.AuditInterceptor;
import com.cameleer3.server.app.interceptor.ProtocolVersionInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@@ -7,17 +8,17 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC configuration.
- *
- * Registers the {@link ProtocolVersionInterceptor} on data and agent endpoint paths,
- * excluding health, API docs, and Swagger UI paths that do not require protocol versioning.
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final ProtocolVersionInterceptor protocolVersionInterceptor;
+ private final AuditInterceptor auditInterceptor;
- public WebConfig(ProtocolVersionInterceptor protocolVersionInterceptor) {
+ public WebConfig(ProtocolVersionInterceptor protocolVersionInterceptor,
+ AuditInterceptor auditInterceptor) {
this.protocolVersionInterceptor = protocolVersionInterceptor;
+ this.auditInterceptor = auditInterceptor;
}
@Override
@@ -33,5 +34,14 @@ public class WebConfig implements WebMvcConfigurer {
"/api/v1/agents/register",
"/api/v1/agents/*/refresh"
);
+
+ // Safety-net audit: catches any unaudited POST/PUT/DELETE
+ registry.addInterceptor(auditInterceptor)
+ .addPathPatterns("/api/v1/**")
+ .excludePathPatterns(
+ "/api/v1/data/**",
+ "/api/v1/agents/*/heartbeat",
+ "/api/v1/health"
+ );
}
}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentCommandController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentCommandController.java
index 6fd1a449..e71dbc3b 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentCommandController.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentCommandController.java
@@ -5,6 +5,9 @@ import com.cameleer3.server.app.dto.CommandAckRequest;
import com.cameleer3.server.app.dto.CommandBroadcastResponse;
import com.cameleer3.server.app.dto.CommandRequest;
import com.cameleer3.server.app.dto.CommandSingleResponse;
+import com.cameleer3.server.core.admin.AuditCategory;
+import com.cameleer3.server.core.admin.AuditResult;
+import com.cameleer3.server.core.admin.AuditService;
import com.cameleer3.server.core.agent.AgentCommand;
import com.cameleer3.server.core.agent.AgentEventService;
import com.cameleer3.server.core.agent.AgentInfo;
@@ -13,6 +16,7 @@ import com.cameleer3.server.core.agent.AgentState;
import com.cameleer3.server.core.agent.CommandType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -51,15 +55,18 @@ public class AgentCommandController {
private final SseConnectionManager connectionManager;
private final ObjectMapper objectMapper;
private final AgentEventService agentEventService;
+ private final AuditService auditService;
public AgentCommandController(AgentRegistryService registryService,
SseConnectionManager connectionManager,
ObjectMapper objectMapper,
- AgentEventService agentEventService) {
+ AgentEventService agentEventService,
+ AuditService auditService) {
this.registryService = registryService;
this.connectionManager = connectionManager;
this.objectMapper = objectMapper;
this.agentEventService = agentEventService;
+ this.auditService = auditService;
}
@PostMapping("/{id}/commands")
@@ -69,7 +76,8 @@ public class AgentCommandController {
@ApiResponse(responseCode = "400", description = "Invalid command payload")
@ApiResponse(responseCode = "404", description = "Agent not registered")
public ResponseEntity sendCommand(@PathVariable String id,
- @RequestBody CommandRequest request) throws JsonProcessingException {
+ @RequestBody CommandRequest request,
+ HttpServletRequest httpRequest) throws JsonProcessingException {
AgentInfo agent = registryService.findById(id);
if (agent == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id);
@@ -81,6 +89,10 @@ public class AgentCommandController {
String status = connectionManager.isConnected(id) ? "DELIVERED" : "PENDING";
+ auditService.log("send_agent_command", AuditCategory.AGENT, id,
+ java.util.Map.of("type", request.type(), "status", status),
+ AuditResult.SUCCESS, httpRequest);
+
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(new CommandSingleResponse(command.id(), status));
}
@@ -91,7 +103,8 @@ public class AgentCommandController {
@ApiResponse(responseCode = "202", description = "Commands accepted")
@ApiResponse(responseCode = "400", description = "Invalid command payload")
public ResponseEntity sendGroupCommand(@PathVariable String group,
- @RequestBody CommandRequest request) throws JsonProcessingException {
+ @RequestBody CommandRequest request,
+ HttpServletRequest httpRequest) throws JsonProcessingException {
CommandType type = mapCommandType(request.type());
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
@@ -106,6 +119,10 @@ public class AgentCommandController {
commandIds.add(command.id());
}
+ auditService.log("broadcast_group_command", AuditCategory.AGENT, group,
+ java.util.Map.of("type", request.type(), "agentCount", agents.size()),
+ AuditResult.SUCCESS, httpRequest);
+
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(new CommandBroadcastResponse(commandIds, agents.size()));
}
@@ -115,7 +132,8 @@ public class AgentCommandController {
description = "Sends a command to all agents currently in LIVE state")
@ApiResponse(responseCode = "202", description = "Commands accepted")
@ApiResponse(responseCode = "400", description = "Invalid command payload")
- public ResponseEntity broadcastCommand(@RequestBody CommandRequest request) throws JsonProcessingException {
+ public ResponseEntity broadcastCommand(@RequestBody CommandRequest request,
+ HttpServletRequest httpRequest) throws JsonProcessingException {
CommandType type = mapCommandType(request.type());
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
@@ -127,6 +145,10 @@ public class AgentCommandController {
commandIds.add(command.id());
}
+ auditService.log("broadcast_all_command", AuditCategory.AGENT, null,
+ java.util.Map.of("type", request.type(), "agentCount", liveAgents.size()),
+ AuditResult.SUCCESS, httpRequest);
+
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(new CommandBroadcastResponse(commandIds, liveAgents.size()));
}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java
index a04f294d..79d8ae26 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java
@@ -8,6 +8,9 @@ import com.cameleer3.server.app.dto.AgentRegistrationRequest;
import com.cameleer3.server.app.dto.AgentRegistrationResponse;
import com.cameleer3.server.app.dto.ErrorResponse;
import com.cameleer3.server.app.security.BootstrapTokenValidator;
+import com.cameleer3.server.core.admin.AuditCategory;
+import com.cameleer3.server.core.admin.AuditResult;
+import com.cameleer3.server.core.admin.AuditService;
import com.cameleer3.server.core.agent.AgentEventService;
import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService;
@@ -58,6 +61,7 @@ public class AgentRegistrationController {
private final JwtService jwtService;
private final Ed25519SigningService ed25519SigningService;
private final AgentEventService agentEventService;
+ private final AuditService auditService;
private final JdbcTemplate jdbc;
public AgentRegistrationController(AgentRegistryService registryService,
@@ -66,6 +70,7 @@ public class AgentRegistrationController {
JwtService jwtService,
Ed25519SigningService ed25519SigningService,
AgentEventService agentEventService,
+ AuditService auditService,
JdbcTemplate jdbc) {
this.registryService = registryService;
this.config = config;
@@ -73,6 +78,7 @@ public class AgentRegistrationController {
this.jwtService = jwtService;
this.ed25519SigningService = ed25519SigningService;
this.agentEventService = agentEventService;
+ this.auditService = auditService;
this.jdbc = jdbc;
}
@@ -113,6 +119,10 @@ public class AgentRegistrationController {
agentEventService.recordEvent(request.agentId(), application, "REGISTERED",
"Agent registered: " + request.name());
+ auditService.log(request.agentId(), "agent_register", AuditCategory.AGENT, request.agentId(),
+ Map.of("application", application, "name", request.name()),
+ AuditResult.SUCCESS, httpRequest);
+
// Issue JWT tokens with AGENT role
List roles = List.of("AGENT");
String accessToken = jwtService.createAccessToken(request.agentId(), application, roles);
@@ -135,7 +145,8 @@ public class AgentRegistrationController {
@ApiResponse(responseCode = "401", description = "Invalid or expired refresh token")
@ApiResponse(responseCode = "404", description = "Agent not found")
public ResponseEntity refresh(@PathVariable String id,
- @RequestBody AgentRefreshRequest request) {
+ @RequestBody AgentRefreshRequest request,
+ HttpServletRequest httpRequest) {
if (request.refreshToken() == null || request.refreshToken().isBlank()) {
return ResponseEntity.status(401).build();
}
@@ -169,6 +180,9 @@ public class AgentRegistrationController {
String newAccessToken = jwtService.createAccessToken(agentId, agent.application(), roles);
String newRefreshToken = jwtService.createRefreshToken(agentId, agent.application(), roles);
+ auditService.log(agentId, "agent_token_refresh", AuditCategory.AUTH, agentId,
+ null, AuditResult.SUCCESS, httpRequest);
+
return ResponseEntity.ok(new AgentRefreshResponse(newAccessToken, newRefreshToken));
}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ApplicationConfigController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ApplicationConfigController.java
index 970db4a9..594aff5d 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ApplicationConfigController.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ApplicationConfigController.java
@@ -2,6 +2,9 @@ package com.cameleer3.server.app.controller;
import com.cameleer3.common.model.ApplicationConfig;
import com.cameleer3.server.app.storage.PostgresApplicationConfigRepository;
+import com.cameleer3.server.core.admin.AuditCategory;
+import com.cameleer3.server.core.admin.AuditResult;
+import com.cameleer3.server.core.admin.AuditService;
import com.cameleer3.server.core.agent.AgentCommand;
import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService;
@@ -12,6 +15,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
@@ -35,20 +39,24 @@ public class ApplicationConfigController {
private final PostgresApplicationConfigRepository configRepository;
private final AgentRegistryService registryService;
private final ObjectMapper objectMapper;
+ private final AuditService auditService;
public ApplicationConfigController(PostgresApplicationConfigRepository configRepository,
AgentRegistryService registryService,
- ObjectMapper objectMapper) {
+ ObjectMapper objectMapper,
+ AuditService auditService) {
this.configRepository = configRepository;
this.registryService = registryService;
this.objectMapper = objectMapper;
+ this.auditService = auditService;
}
@GetMapping
@Operation(summary = "List all application configs",
description = "Returns stored configurations for all applications")
@ApiResponse(responseCode = "200", description = "Configs returned")
- public ResponseEntity> listConfigs() {
+ public ResponseEntity> listConfigs(HttpServletRequest httpRequest) {
+ auditService.log("view_app_configs", AuditCategory.CONFIG, null, null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(configRepository.findAll());
}
@@ -56,7 +64,9 @@ public class ApplicationConfigController {
@Operation(summary = "Get application config",
description = "Returns the current configuration for an application. Returns defaults if none stored.")
@ApiResponse(responseCode = "200", description = "Config returned")
- public ResponseEntity getConfig(@PathVariable String application) {
+ public ResponseEntity getConfig(@PathVariable String application,
+ HttpServletRequest httpRequest) {
+ auditService.log("view_app_config", AuditCategory.CONFIG, application, null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(
configRepository.findByApplication(application)
.orElse(defaultConfig(application)));
@@ -68,7 +78,8 @@ public class ApplicationConfigController {
@ApiResponse(responseCode = "200", description = "Config saved and pushed")
public ResponseEntity updateConfig(@PathVariable String application,
@RequestBody ApplicationConfig config,
- Authentication auth) {
+ Authentication auth,
+ HttpServletRequest httpRequest) {
String updatedBy = auth != null ? auth.getName() : "system";
config.setApplication(application);
@@ -77,6 +88,10 @@ public class ApplicationConfigController {
int pushed = pushConfigToAgents(application, saved);
log.info("Config v{} saved for '{}', pushed to {} agent(s)", saved.getVersion(), application, pushed);
+ auditService.log("update_app_config", AuditCategory.CONFIG, application,
+ Map.of("version", saved.getVersion(), "agentsPushed", pushed),
+ AuditResult.SUCCESS, httpRequest);
+
return ResponseEntity.ok(saved);
}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AuditLogController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AuditLogController.java
index 87663c6a..e0ee7db4 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AuditLogController.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AuditLogController.java
@@ -5,8 +5,11 @@ import com.cameleer3.server.core.admin.AuditCategory;
import com.cameleer3.server.core.admin.AuditRepository;
import com.cameleer3.server.core.admin.AuditRepository.AuditPage;
import com.cameleer3.server.core.admin.AuditRepository.AuditQuery;
+import com.cameleer3.server.core.admin.AuditResult;
+import com.cameleer3.server.core.admin.AuditService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -26,14 +29,17 @@ import java.time.ZoneOffset;
public class AuditLogController {
private final AuditRepository auditRepository;
+ private final AuditService auditService;
- public AuditLogController(AuditRepository auditRepository) {
+ public AuditLogController(AuditRepository auditRepository, AuditService auditService) {
this.auditRepository = auditRepository;
+ this.auditService = auditService;
}
@GetMapping
@Operation(summary = "Search audit log entries with pagination")
public ResponseEntity getAuditLog(
+ HttpServletRequest httpRequest,
@RequestParam(required = false) String username,
@RequestParam(required = false) String category,
@RequestParam(required = false) String search,
@@ -58,6 +64,8 @@ public class AuditLogController {
}
}
+ auditService.log("view_audit_log", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
+
AuditQuery query = new AuditQuery(username, cat, search, fromInstant, toInstant, sort, order, page, size);
AuditPage result = auditRepository.find(query);
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/OidcConfigAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/OidcConfigAdminController.java
index 1fbd445c..d7407c26 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/OidcConfigAdminController.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/OidcConfigAdminController.java
@@ -61,7 +61,8 @@ public class OidcConfigAdminController {
@GetMapping
@Operation(summary = "Get OIDC configuration")
@ApiResponse(responseCode = "200", description = "Current OIDC configuration (client_secret masked)")
- public ResponseEntity getConfig() {
+ public ResponseEntity getConfig(HttpServletRequest httpRequest) {
+ auditService.log("view_oidc_config", AuditCategory.CONFIG, null, null, AuditResult.SUCCESS, httpRequest);
Optional config = configRepository.find();
if (config.isEmpty()) {
return ResponseEntity.ok(OidcAdminConfigResponse.unconfigured());
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java
index 6d9666fc..6c0a4859 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java
@@ -58,7 +58,8 @@ public class UserAdminController {
@GetMapping
@Operation(summary = "List all users with RBAC detail")
@ApiResponse(responseCode = "200", description = "User list returned")
- public ResponseEntity> listUsers() {
+ public ResponseEntity> listUsers(HttpServletRequest httpRequest) {
+ auditService.log("view_users", AuditCategory.USER_MGMT, null, null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(rbacService.listUsers());
}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/interceptor/AuditInterceptor.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/interceptor/AuditInterceptor.java
new file mode 100644
index 00000000..ee31bd98
--- /dev/null
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/interceptor/AuditInterceptor.java
@@ -0,0 +1,53 @@
+package com.cameleer3.server.app.interceptor;
+
+import com.cameleer3.server.core.admin.AuditCategory;
+import com.cameleer3.server.core.admin.AuditResult;
+import com.cameleer3.server.core.admin.AuditService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Safety-net audit interceptor that logs a basic entry for any state-changing
+ * request (POST/PUT/DELETE) that was not explicitly audited by the controller.
+ *
+ * Controllers that call {@link AuditService#log} set the {@code audit.logged}
+ * request attribute, which this interceptor checks to avoid double-recording.
+ */
+@Component
+public class AuditInterceptor implements HandlerInterceptor {
+
+ private static final Set AUDITABLE_METHODS = Set.of("POST", "PUT", "DELETE");
+
+ private final AuditService auditService;
+
+ public AuditInterceptor(AuditService auditService) {
+ this.auditService = auditService;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
+ Object handler, Exception ex) {
+ if (!AUDITABLE_METHODS.contains(request.getMethod())) {
+ return;
+ }
+ if (Boolean.TRUE.equals(request.getAttribute("audit.logged"))) {
+ return;
+ }
+
+ String path = request.getRequestURI();
+ AuditResult result = response.getStatus() < 400 ? AuditResult.SUCCESS : AuditResult.FAILURE;
+
+ auditService.log(
+ "HTTP " + request.getMethod() + " " + path,
+ AuditCategory.INFRA,
+ path,
+ Map.of("status", response.getStatus()),
+ result,
+ request);
+ }
+}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java
index 7d5f1e0d..c66b424f 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java
@@ -159,6 +159,9 @@ public class OidcAuthController {
throw e;
} catch (Exception e) {
log.error("OIDC callback failed: {}", e.getMessage(), e);
+ auditService.log("unknown", "login_oidc", AuditCategory.AUTH, null,
+ Map.of("reason", e.getMessage() != null ? e.getMessage() : "unknown"),
+ AuditResult.FAILURE, httpRequest);
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"OIDC authentication failed: " + e.getMessage());
}
diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java
index f3c85998..6049514b 100644
--- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java
+++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/UiAuthController.java
@@ -123,7 +123,8 @@ public class UiAuthController {
@ApiResponse(responseCode = "200", description = "Token refreshed")
@ApiResponse(responseCode = "401", description = "Invalid refresh token",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
- public ResponseEntity refresh(@RequestBody RefreshRequest request) {
+ public ResponseEntity refresh(@RequestBody RefreshRequest request,
+ HttpServletRequest httpRequest) {
try {
JwtValidationResult result = jwtService.validateRefreshToken(request.refreshToken());
if (!result.subject().startsWith("user:")) {
@@ -138,6 +139,7 @@ public class UiAuthController {
String displayName = userRepository.findById(result.subject())
.map(UserInfo::displayName)
.orElse(result.subject());
+ auditService.log(result.subject(), "token_refresh", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, null));
} catch (ResponseStatusException e) {
throw e;
diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/AuditCategory.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/AuditCategory.java
index 221963f1..1e365da3 100644
--- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/AuditCategory.java
+++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/AuditCategory.java
@@ -1,5 +1,5 @@
package com.cameleer3.server.core.admin;
public enum AuditCategory {
- INFRA, AUTH, USER_MGMT, CONFIG, RBAC
+ INFRA, AUTH, USER_MGMT, CONFIG, RBAC, AGENT
}
diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/AuditService.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/AuditService.java
index 5a5b3324..5dde748a 100644
--- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/AuditService.java
+++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/admin/AuditService.java
@@ -34,6 +34,10 @@ public class AuditService {
repository.insert(record);
+ if (request != null) {
+ request.setAttribute("audit.logged", true);
+ }
+
log.info("AUDIT: user={} action={} category={} target={} result={}",
username, action, category, target, result);
}
diff --git a/ui/src/pages/Admin/AuditLogPage.tsx b/ui/src/pages/Admin/AuditLogPage.tsx
index 2fb77dcb..bdf589ec 100644
--- a/ui/src/pages/Admin/AuditLogPage.tsx
+++ b/ui/src/pages/Admin/AuditLogPage.tsx
@@ -13,6 +13,7 @@ const CATEGORIES = [
{ value: 'USER_MGMT', label: 'USER_MGMT' },
{ value: 'CONFIG', label: 'CONFIG' },
{ value: 'RBAC', label: 'RBAC' },
+ { value: 'AGENT', label: 'AGENT' },
];
function formatTimestamp(iso: string): string {