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 6e86c289..29242404 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 @@ -1,6 +1,7 @@ package com.cameleer3.server.app.controller; import com.cameleer3.common.model.ApplicationConfig; +import com.cameleer3.server.app.dto.AppConfigResponse; import com.cameleer3.server.app.dto.CommandGroupResponse; import com.cameleer3.server.app.dto.ConfigUpdateResponse; import com.cameleer3.server.app.dto.TestExpressionRequest; @@ -9,6 +10,9 @@ 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.admin.SensitiveKeysConfig; +import com.cameleer3.server.core.admin.SensitiveKeysMerger; +import com.cameleer3.server.core.admin.SensitiveKeysRepository; import com.cameleer3.server.core.agent.AgentInfo; import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.agent.AgentState; @@ -52,17 +56,20 @@ public class ApplicationConfigController { private final ObjectMapper objectMapper; private final AuditService auditService; private final DiagramStore diagramStore; + private final SensitiveKeysRepository sensitiveKeysRepository; public ApplicationConfigController(PostgresApplicationConfigRepository configRepository, AgentRegistryService registryService, ObjectMapper objectMapper, AuditService auditService, - DiagramStore diagramStore) { + DiagramStore diagramStore, + SensitiveKeysRepository sensitiveKeysRepository) { this.configRepository = configRepository; this.registryService = registryService; this.objectMapper = objectMapper; this.auditService = auditService; this.diagramStore = diagramStore; + this.sensitiveKeysRepository = sensitiveKeysRepository; } @GetMapping @@ -76,14 +83,20 @@ public class ApplicationConfigController { @GetMapping("/{application}") @Operation(summary = "Get application config", - description = "Returns the current configuration for an application. Returns defaults if none stored.") + description = "Returns the current configuration for an application with merged sensitive keys.") @ApiResponse(responseCode = "200", description = "Config returned") - public ResponseEntity getConfig(@PathVariable String application, - HttpServletRequest httpRequest) { + 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))); + ApplicationConfig config = configRepository.findByApplication(application) + .orElse(defaultConfig(application)); + + List globalKeys = sensitiveKeysRepository.find() + .map(SensitiveKeysConfig::keys) + .orElse(null); + List merged = SensitiveKeysMerger.merge(globalKeys, extractSensitiveKeys(config)); + + return ResponseEntity.ok(new AppConfigResponse(config, globalKeys, merged)); } @PutMapping("/{application}") @@ -100,7 +113,15 @@ public class ApplicationConfigController { config.setApplication(application); ApplicationConfig saved = configRepository.save(application, config, updatedBy); - CommandGroupResponse pushResult = pushConfigToAgents(application, environment, saved); + // Merge global + per-app sensitive keys for the SSE push payload + List globalKeys = sensitiveKeysRepository.find() + .map(SensitiveKeysConfig::keys) + .orElse(null); + List perAppKeys = extractSensitiveKeys(saved); + List mergedKeys = SensitiveKeysMerger.merge(globalKeys, perAppKeys); + + // Push with merged sensitive keys injected into the payload + CommandGroupResponse pushResult = pushConfigToAgentsWithMergedKeys(application, environment, saved, mergedKeys); log.info("Config v{} saved for '{}', pushed to {} agent(s), {} responded", saved.getVersion(), application, pushResult.total(), pushResult.responded()); @@ -180,12 +201,37 @@ public class ApplicationConfigController { } } - private CommandGroupResponse pushConfigToAgents(String application, String environment, ApplicationConfig config) { + /** + * Extracts sensitiveKeys from ApplicationConfig via JsonNode to avoid compile-time + * dependency on getSensitiveKeys() which may not be in the published cameleer3-common jar yet. + */ + private List extractSensitiveKeys(ApplicationConfig config) { + try { + com.fasterxml.jackson.databind.JsonNode node = objectMapper.valueToTree(config); + com.fasterxml.jackson.databind.JsonNode keysNode = node.get("sensitiveKeys"); + if (keysNode == null || keysNode.isNull() || !keysNode.isArray()) { + return null; + } + return objectMapper.convertValue(keysNode, new com.fasterxml.jackson.core.type.TypeReference>() {}); + } catch (Exception e) { + return null; + } + } + + /** + * Push config to agents with merged sensitive keys injected into the JSON payload. + */ + private CommandGroupResponse pushConfigToAgentsWithMergedKeys(String application, String environment, + ApplicationConfig config, List mergedKeys) { String payloadJson; try { - payloadJson = objectMapper.writeValueAsString(config); - } catch (JsonProcessingException e) { - log.error("Failed to serialize config for push", e); + // Serialize config to a mutable map, inject merged keys + @SuppressWarnings("unchecked") + Map configMap = objectMapper.convertValue(config, Map.class); + configMap.put("sensitiveKeys", mergedKeys); + payloadJson = objectMapper.writeValueAsString(configMap); + } catch (Exception e) { + log.error("Failed to serialize config with merged keys for push", e); return new CommandGroupResponse(false, 0, 0, List.of(), List.of()); } @@ -196,7 +242,6 @@ public class ApplicationConfigController { return new CommandGroupResponse(true, 0, 0, List.of(), List.of()); } - // Wait with shared 10-second deadline long deadline = System.currentTimeMillis() + 10_000; List responses = new ArrayList<>(); List timedOut = new ArrayList<>(); diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AppConfigResponse.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AppConfigResponse.java new file mode 100644 index 00000000..b1898f1a --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/AppConfigResponse.java @@ -0,0 +1,14 @@ +package com.cameleer3.server.app.dto; + +import com.cameleer3.common.model.ApplicationConfig; + +import java.util.List; + +/** + * Wraps ApplicationConfig with additional server-computed fields for the UI. + */ +public record AppConfigResponse( + ApplicationConfig config, + List globalSensitiveKeys, + List mergedSensitiveKeys +) {}