Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4351318f89 | ||
|
|
b27c46d72b | ||
|
|
b0ecc22260 | ||
|
|
91279e85fa | ||
|
|
55fb5cf6c4 | ||
|
|
9c3d1eeca6 | ||
|
|
a79efd8d39 | ||
|
|
ade36f7d86 | ||
|
|
8b69a5fc05 | ||
|
|
ae8c2ec293 | ||
|
|
63b00824bb |
+121
@@ -0,0 +1,121 @@
|
||||
package stirling.software.proprietary.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.api.PipelineApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
@Slf4j
|
||||
@PipelineApi
|
||||
public class PipelineConfigController {
|
||||
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public PipelineConfigController(
|
||||
RuntimePathConfig runtimePathConfig, ObjectMapper objectMapper) {
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostMapping("/watched-folders/config")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Operation(
|
||||
summary =
|
||||
"Save a folder-scanning pipeline config into pipeline/watchedFolders (optionally in a subfolder)")
|
||||
public ResponseEntity<?> saveConfigToWatchedFolder(
|
||||
@RequestBody SaveWatchedFolderConfigRequest request) {
|
||||
try {
|
||||
if (request == null || request.getConfig() == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Missing request body or config payload"));
|
||||
}
|
||||
|
||||
Path watchedRoot =
|
||||
Paths.get(runtimePathConfig.getPipelineWatchedFoldersPath())
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
|
||||
String subfolder = request.getSubfolder() == null ? "" : request.getSubfolder().trim();
|
||||
Path targetDir =
|
||||
subfolder.isEmpty()
|
||||
? watchedRoot
|
||||
: watchedRoot.resolve(subfolder).toAbsolutePath().normalize();
|
||||
|
||||
if (!targetDir.startsWith(watchedRoot)) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid subfolder path"));
|
||||
}
|
||||
|
||||
Files.createDirectories(targetDir);
|
||||
|
||||
String safeFileName = sanitizeFileName(request.getFileName());
|
||||
Path targetFile = targetDir.resolve(safeFileName).toAbsolutePath().normalize();
|
||||
|
||||
if (!targetFile.startsWith(watchedRoot)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Invalid target file path"));
|
||||
}
|
||||
|
||||
String json =
|
||||
objectMapper
|
||||
.writerWithDefaultPrettyPrinter()
|
||||
.writeValueAsString(request.getConfig());
|
||||
Files.writeString(targetFile, json, StandardCharsets.UTF_8);
|
||||
|
||||
return ResponseEntity.ok(
|
||||
Map.of(
|
||||
"success",
|
||||
true,
|
||||
"savedPath",
|
||||
targetFile.toString(),
|
||||
"fileName",
|
||||
safeFileName));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to write pipeline config to watched folder", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "Failed to write config file"));
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeFileName(String fileName) {
|
||||
String base =
|
||||
fileName == null || fileName.isBlank()
|
||||
? "automation.folder-scan.json"
|
||||
: fileName.trim();
|
||||
String sanitized = base.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
if (!sanitized.toLowerCase().endsWith(".json")) {
|
||||
sanitized = sanitized + ".json";
|
||||
}
|
||||
if (sanitized.length() > 200) {
|
||||
sanitized = sanitized.substring(0, 200);
|
||||
if (!sanitized.toLowerCase().endsWith(".json")) {
|
||||
sanitized = sanitized + ".json";
|
||||
}
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SaveWatchedFolderConfigRequest {
|
||||
private String subfolder;
|
||||
private String fileName;
|
||||
private Object config;
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package stirling.software.proprietary.controller.api;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.ObjectWriter;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PipelineConfigControllerTest {
|
||||
|
||||
@Mock private RuntimePathConfig runtimePathConfig;
|
||||
@Mock private ObjectMapper objectMapper;
|
||||
@Mock private ObjectWriter objectWriter;
|
||||
|
||||
@TempDir Path tempDir;
|
||||
|
||||
@Test
|
||||
void saveConfigToWatchedFolder_returnsBadRequest_whenRequestIsNull() {
|
||||
PipelineConfigController controller =
|
||||
new PipelineConfigController(runtimePathConfig, objectMapper);
|
||||
|
||||
ResponseEntity<?> response = controller.saveConfigToWatchedFolder(null);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
Map<?, ?> body = (Map<?, ?>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertEquals("Missing request body or config payload", body.get("error"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveConfigToWatchedFolder_returnsBadRequest_whenConfigIsNull() {
|
||||
PipelineConfigController controller =
|
||||
new PipelineConfigController(runtimePathConfig, objectMapper);
|
||||
PipelineConfigController.SaveWatchedFolderConfigRequest request =
|
||||
new PipelineConfigController.SaveWatchedFolderConfigRequest();
|
||||
request.setConfig(null);
|
||||
|
||||
ResponseEntity<?> response = controller.saveConfigToWatchedFolder(request);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
Map<?, ?> body = (Map<?, ?>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertEquals("Missing request body or config payload", body.get("error"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveConfigToWatchedFolder_returnsBadRequest_whenSubfolderEscapesWatchedRoot() {
|
||||
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
|
||||
PipelineConfigController controller =
|
||||
new PipelineConfigController(runtimePathConfig, objectMapper);
|
||||
|
||||
PipelineConfigController.SaveWatchedFolderConfigRequest request =
|
||||
new PipelineConfigController.SaveWatchedFolderConfigRequest();
|
||||
request.setSubfolder("../outside");
|
||||
request.setFileName("config");
|
||||
request.setConfig(Map.of("enabled", true));
|
||||
|
||||
ResponseEntity<?> response = controller.saveConfigToWatchedFolder(request);
|
||||
|
||||
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||
Map<?, ?> body = (Map<?, ?>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertEquals("Invalid subfolder path", body.get("error"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveConfigToWatchedFolder_savesFileAndReturnsSuccess() throws IOException {
|
||||
when(runtimePathConfig.getPipelineWatchedFoldersPath()).thenReturn(tempDir.toString());
|
||||
when(objectMapper.writerWithDefaultPrettyPrinter()).thenReturn(objectWriter);
|
||||
when(objectWriter.writeValueAsString(any())).thenReturn("{\"k\":\"v\"}");
|
||||
PipelineConfigController controller =
|
||||
new PipelineConfigController(runtimePathConfig, objectMapper);
|
||||
|
||||
PipelineConfigController.SaveWatchedFolderConfigRequest request =
|
||||
new PipelineConfigController.SaveWatchedFolderConfigRequest();
|
||||
request.setSubfolder("incoming");
|
||||
request.setFileName("my:config");
|
||||
request.setConfig(Map.of("k", "v"));
|
||||
|
||||
ResponseEntity<?> response = controller.saveConfigToWatchedFolder(request);
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
Map<?, ?> body = (Map<?, ?>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertEquals(true, body.get("success"));
|
||||
assertEquals("my_config.json", body.get("fileName"));
|
||||
|
||||
Path expectedFile = tempDir.resolve("incoming").resolve("my_config.json");
|
||||
assertTrue(Files.exists(expectedFile));
|
||||
assertEquals("{\"k\":\"v\"}", Files.readString(expectedFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveConfigToWatchedFolder_returnsInternalServerError_whenWriteFails() throws IOException {
|
||||
Path watchedRootAsFile = tempDir.resolve("watched-root-file");
|
||||
Files.writeString(watchedRootAsFile, "not-a-directory");
|
||||
when(runtimePathConfig.getPipelineWatchedFoldersPath())
|
||||
.thenReturn(watchedRootAsFile.toString());
|
||||
PipelineConfigController controller =
|
||||
new PipelineConfigController(runtimePathConfig, objectMapper);
|
||||
|
||||
PipelineConfigController.SaveWatchedFolderConfigRequest request =
|
||||
new PipelineConfigController.SaveWatchedFolderConfigRequest();
|
||||
request.setSubfolder("incoming");
|
||||
request.setFileName("config");
|
||||
request.setConfig(Map.of("k", "v"));
|
||||
|
||||
ResponseEntity<?> response = controller.saveConfigToWatchedFolder(request);
|
||||
|
||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
|
||||
Map<?, ?> body = (Map<?, ?>) response.getBody();
|
||||
assertNotNull(body);
|
||||
assertEquals("Failed to write config file", body.get("error"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user