From 0d94132c9864c913ba81ece600a387c2d0517883 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:41:10 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20SOC2=20audit=20log=20completeness=20?= =?UTF-8?q?=E2=80=94=20hybrid=20interceptor=20+=20explicit=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AuditInterceptor as a safety net that auto-audits any POST/PUT/DELETE without an explicit audit call (excludes data ingestion + heartbeat). AuditService sets a request attribute so the interceptor skips when explicit logging already happened. New explicit audit calls: - ApplicationConfigController: view/update app config - AgentCommandController: send/broadcast commands (AGENT category) - AgentRegistrationController: agent register + token refresh - UiAuthController: UI token refresh - OidcAuthController: OIDC callback failure - AuditLogController: view audit log (sensitive read) - UserAdminController: view users (sensitive read) - OidcConfigAdminController: view OIDC config (sensitive read) New AuditCategory.AGENT added. Frontend audit log filter updated. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../server/app/config/WebConfig.java | 18 +++++-- .../controller/AgentCommandController.java | 30 +++++++++-- .../AgentRegistrationController.java | 16 +++++- .../ApplicationConfigController.java | 23 ++++++-- .../app/controller/AuditLogController.java | 10 +++- .../controller/OidcConfigAdminController.java | 3 +- .../app/controller/UserAdminController.java | 3 +- .../app/interceptor/AuditInterceptor.java | 53 +++++++++++++++++++ .../app/security/OidcAuthController.java | 3 ++ .../server/app/security/UiAuthController.java | 4 +- .../server/core/admin/AuditCategory.java | 2 +- .../server/core/admin/AuditService.java | 4 ++ ui/src/pages/Admin/AuditLogPage.tsx | 1 + 13 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/interceptor/AuditInterceptor.java 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 {