remove file endpoint, show delta cahgned to tracking instead

This commit is contained in:
Anthony Stirling 2025-07-28 12:18:19 +01:00
parent f9d36b985a
commit 26a6534ae4

View File

@ -1,11 +1,9 @@
package stirling.software.proprietary.security.controller.api;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.springframework.http.HttpStatus;
@ -17,10 +15,10 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.HtmlUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -32,7 +30,6 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.util.GeneralUtils;
import stirling.software.proprietary.security.model.api.admin.SettingValueResponse;
@ -49,11 +46,14 @@ public class AdminSettingsController {
private final ApplicationProperties applicationProperties;
private final ObjectMapper objectMapper;
// Track settings that have been modified but not yet applied (require restart)
private static final ConcurrentHashMap<String, Object> pendingChanges = new ConcurrentHashMap<>();
@GetMapping
@Operation(
summary = "Get all application settings",
description = "Retrieve all current application settings. Admin access required.")
description = "Retrieve all current application settings. Use includePending=true to include settings that will take effect after restart. Admin access required.")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "Settings retrieved successfully"),
@ -61,116 +61,44 @@ public class AdminSettingsController {
responseCode = "403",
description = "Access denied - Admin role required")
})
public ResponseEntity<ApplicationProperties> getSettings() {
log.debug("Admin requested all application settings");
return ResponseEntity.ok(applicationProperties);
}
@GetMapping("/file")
@Operation(
summary = "Get settings file as JSON",
description =
"Retrieve the settings.yml file parsed as JSON, showing the latest saved values (after restart). Comments and formatting are ignored. Admin access required.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "Settings file retrieved and parsed successfully"),
@ApiResponse(responseCode = "404", description = "Settings file not found"),
@ApiResponse(
responseCode = "403",
description = "Access denied - Admin role required"),
@ApiResponse(
responseCode = "500",
description = "Failed to read or parse settings file")
})
public ResponseEntity<?> getSettingsFile() {
try {
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
if (!Files.exists(settingsPath)) {
return ResponseEntity.notFound().build();
}
// Parse YAML file to JSON
ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
ObjectMapper jsonMapper = new ObjectMapper();
Object yamlData = yamlMapper.readValue(settingsPath.toFile(), Object.class);
log.debug("Admin requested settings file as JSON");
return ResponseEntity.ok(yamlData);
} catch (IOException e) {
log.error("Failed to read or parse settings file: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to read or parse settings file: " + e.getMessage());
} catch (Exception e) {
log.error("Unexpected error reading settings file: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Unexpected error reading settings file");
public ResponseEntity<?> getSettings(@RequestParam(defaultValue = "false") boolean includePending) {
log.debug("Admin requested all application settings (includePending={})", includePending);
if (!includePending) {
return ResponseEntity.ok(applicationProperties);
}
// Include pending changes in response
Map<String, Object> response = new HashMap<>();
response.put("currentSettings", applicationProperties);
response.put("pendingChanges", pendingChanges);
response.put("hasPendingChanges", !pendingChanges.isEmpty());
return ResponseEntity.ok(response);
}
@GetMapping("/delta")
@Operation(
summary = "Get settings delta (pending changes)",
summary = "Get pending settings changes",
description =
"Compare current runtime settings with saved file settings to show pending changes that will take effect after restart. Admin access required.")
"Retrieve settings that have been modified but not yet applied (require restart). Admin access required.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "Settings delta retrieved successfully"),
@ApiResponse(responseCode = "404", description = "Settings file not found"),
description = "Pending changes retrieved successfully"),
@ApiResponse(
responseCode = "403",
description = "Access denied - Admin role required"),
@ApiResponse(
responseCode = "500",
description = "Failed to calculate settings delta")
description = "Access denied - Admin role required")
})
public ResponseEntity<?> getSettingsDelta() {
try {
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
if (!Files.exists(settingsPath)) {
return ResponseEntity.notFound().build();
}
// Get current runtime settings as JSON
Map<String, Object> runtimeSettings =
objectMapper.convertValue(applicationProperties, Map.class);
// Parse YAML file to get saved settings
ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
Object fileData = yamlMapper.readValue(settingsPath.toFile(), Object.class);
Map<String, Object> savedSettings = objectMapper.convertValue(fileData, Map.class);
// Calculate differences
Map<String, Object> delta = new HashMap<>();
Map<String, Object> pendingChanges = new HashMap<>();
Map<String, Object> currentValues = new HashMap<>();
findDifferences("", runtimeSettings, savedSettings, pendingChanges, currentValues);
delta.put(
"pendingChanges", pendingChanges); // Values that will take effect after restart
delta.put("currentValues", currentValues); // Current runtime values
delta.put("hasPendingChanges", !pendingChanges.isEmpty());
log.debug(
"Admin requested settings delta - found {} pending changes",
pendingChanges.size());
return ResponseEntity.ok(delta);
} catch (IOException e) {
log.error("Failed to calculate settings delta: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to calculate settings delta: " + e.getMessage());
} catch (Exception e) {
log.error("Unexpected error calculating settings delta: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Unexpected error calculating settings delta");
}
Map<String, Object> response = new HashMap<>();
response.put("pendingChanges", pendingChanges);
response.put("hasPendingChanges", !pendingChanges.isEmpty());
response.put("count", pendingChanges.size());
log.debug("Admin requested pending changes - found {} settings", pendingChanges.size());
return ResponseEntity.ok(response);
}
@PutMapping
@ -209,6 +137,10 @@ public class AdminSettingsController {
log.info("Admin updating setting: {} = {}", key, value);
GeneralUtils.saveKeyToSettings(key, value);
// Track this as a pending change
pendingChanges.put(key, value);
updatedCount++;
}
@ -314,6 +246,10 @@ public class AdminSettingsController {
log.info("Admin updating section setting: {} = {}", fullKey, value);
GeneralUtils.saveKeyToSettings(fullKey, value);
// Track this as a pending change
pendingChanges.put(fullKey, value);
updatedCount++;
}
@ -401,6 +337,9 @@ public class AdminSettingsController {
Object value = request.getValue();
log.info("Admin updating single setting: {} = {}", key, value);
GeneralUtils.saveKeyToSettings(key, value);
// Track this as a pending change
pendingChanges.put(key, value);
String escapedKey = HtmlUtils.htmlEscape(key);
return ResponseEntity.ok(
@ -557,49 +496,4 @@ public class AdminSettingsController {
}
}
/** Recursively compare two maps to find differences between runtime and saved settings */
@SuppressWarnings("unchecked")
private void findDifferences(
String keyPrefix,
Map<String, Object> runtime,
Map<String, Object> saved,
Map<String, Object> pendingChanges,
Map<String, Object> currentValues) {
// Check all keys in saved settings (these are the pending changes)
for (Map.Entry<String, Object> entry : saved.entrySet()) {
String key = entry.getKey();
String fullKey = keyPrefix.isEmpty() ? key : keyPrefix + "." + key;
Object savedValue = entry.getValue();
Object runtimeValue = runtime.get(key);
if (savedValue instanceof Map && runtimeValue instanceof Map) {
// Recursively check nested objects
findDifferences(
fullKey,
(Map<String, Object>) runtimeValue,
(Map<String, Object>) savedValue,
pendingChanges,
currentValues);
} else {
// Compare values - if they're different, savedValue is pending
if (!java.util.Objects.equals(runtimeValue, savedValue)) {
pendingChanges.put(fullKey, savedValue);
currentValues.put(fullKey, runtimeValue);
}
}
}
// Check for keys that exist in runtime but not in saved (these would be removed on restart)
for (Map.Entry<String, Object> entry : runtime.entrySet()) {
String key = entry.getKey();
String fullKey = keyPrefix.isEmpty() ? key : keyPrefix + "." + key;
if (!saved.containsKey(key)) {
// This runtime setting would be lost on restart
pendingChanges.put(fullKey + " (will be removed)", null);
currentValues.put(fullKey, entry.getValue());
}
}
}
}