mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
fix(policy): guard watched-folder migration and declare its schema owner
This commit is contained in:
+28
@@ -68,6 +68,7 @@ public class WatchedFolderPipelineImport implements MigratedWatchedFolders {
|
||||
private final PolicyTriggerManager policyTriggerManager;
|
||||
private final TeamRepository teamRepository;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
private final FolderAccessGuard folderAccessGuard;
|
||||
|
||||
@Override
|
||||
public boolean isMigrated(Path directory) {
|
||||
@@ -82,6 +83,18 @@ public class WatchedFolderPipelineImport implements MigratedWatchedFolders {
|
||||
@Order(3)
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void importWatchedFolders() {
|
||||
try {
|
||||
convertWatchedFolders();
|
||||
} catch (Exception e) {
|
||||
// A ready-event listener that throws aborts SpringApplication.run; a cosmetic
|
||||
// migration must never be able to stop the app booting.
|
||||
log.error(
|
||||
"Watched-folder conversion failed; leaving the folders to the legacy scanner",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private void convertWatchedFolders() {
|
||||
Long teamId = defaultTeamId();
|
||||
int imported = 0;
|
||||
for (String root : runtimePathConfig.getPipelineWatchedFoldersPaths()) {
|
||||
@@ -123,6 +136,21 @@ public class WatchedFolderPipelineImport implements MigratedWatchedFolders {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The sink checks this on every delivery; converting a folder it would refuse just swaps a
|
||||
// working legacy automation for a pipeline that fails every run.
|
||||
Path outputDirectory = converter.resolveOutputDirectory(config, directory);
|
||||
try {
|
||||
folderAccessGuard.requirePermitted(outputDirectory);
|
||||
} catch (IllegalArgumentException notPermitted) {
|
||||
log.warn(
|
||||
"Watched folder {} writes to {}, which folder access does not permit ({});"
|
||||
+ " leaving it to the legacy scanner",
|
||||
directory,
|
||||
outputDirectory,
|
||||
notPermitted.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
Source input = inputSourceFor(directory, teamId);
|
||||
Source destination = destinationSourceFor(config, directory, teamId);
|
||||
Policy policy =
|
||||
|
||||
+2
-1
@@ -106,7 +106,8 @@ public record Policy(
|
||||
|
||||
/** A copy under a different owner (e.g. moving a seed off a placeholder name). */
|
||||
public Policy withOwner(String newOwner) {
|
||||
return new Policy(id, name, newOwner, enabled, inputs, steps, output, outputIds, teamId);
|
||||
return new Policy(
|
||||
id, name, newOwner, enabled, inputs, steps, output, outputIds, teamId, origin);
|
||||
}
|
||||
|
||||
/** A copy referencing the given saved output destinations. */
|
||||
|
||||
+45
-3
@@ -1,9 +1,12 @@
|
||||
package stirling.software.proprietary.policy.legacy;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -16,8 +19,11 @@ import java.util.Optional;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.dao.DataAccessResourceFailureException;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.proprietary.policy.config.FolderAccessDeniedException;
|
||||
import stirling.software.proprietary.policy.config.FolderAccessGuard;
|
||||
import stirling.software.proprietary.policy.model.PipelineInput;
|
||||
import stirling.software.proprietary.policy.model.Policy;
|
||||
import stirling.software.proprietary.policy.source.InProcessSourceStore;
|
||||
@@ -41,6 +47,7 @@ class WatchedFolderPipelineImportTest {
|
||||
private final RuntimePathConfig runtimePathConfig = mock(RuntimePathConfig.class);
|
||||
private final TeamRepository teamRepository = mock(TeamRepository.class);
|
||||
private final PolicyTriggerManager policyTriggerManager = mock(PolicyTriggerManager.class);
|
||||
private final FolderAccessGuard folderAccessGuard = mock(FolderAccessGuard.class);
|
||||
|
||||
private WatchedFolderPipelineImport importer;
|
||||
private String finishedFolders;
|
||||
@@ -62,7 +69,8 @@ class WatchedFolderPipelineImportTest {
|
||||
importedPipelines,
|
||||
policyTriggerManager,
|
||||
teamRepository,
|
||||
runtimePathConfig);
|
||||
runtimePathConfig,
|
||||
folderAccessGuard);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -176,8 +184,41 @@ class WatchedFolderPipelineImportTest {
|
||||
assertFalse(importer.isMigrated(watchedRoot.resolve("empty")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void leavesAFolderAloneWhenItsOutputDirectoryIsNotPermitted() throws IOException {
|
||||
Path folder = watchedFolderWritingTo("invoices", "/data/outbox");
|
||||
doThrow(new FolderAccessDeniedException("denied"))
|
||||
.when(folderAccessGuard)
|
||||
.requirePermitted(any());
|
||||
|
||||
importer.importWatchedFolders();
|
||||
|
||||
// The sink would refuse every delivery, so the legacy scanner has to keep the folder.
|
||||
assertTrue(policyStore.all().isEmpty());
|
||||
assertTrue(sourceStore.all().isEmpty());
|
||||
assertFalse(importer.isMigrated(folder));
|
||||
assertTrue(
|
||||
Files.exists(folder.resolve("config.json")),
|
||||
"the legacy scanner still needs its config");
|
||||
}
|
||||
|
||||
@Test
|
||||
void neverFailsBootWhenTheTeamLookupBlowsUp() throws IOException {
|
||||
watchedFolder("invoices");
|
||||
when(teamRepository.findByName(org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenThrow(new DataAccessResourceFailureException("db down"));
|
||||
|
||||
// A ready-event listener that throws aborts SpringApplication.run.
|
||||
assertDoesNotThrow(() -> importer.importWatchedFolders());
|
||||
assertTrue(policyStore.all().isEmpty());
|
||||
}
|
||||
|
||||
/** A legacy watched folder: a pipeline JSON beside the files it processes. */
|
||||
private Path watchedFolder(String relative) throws IOException {
|
||||
return watchedFolderWritingTo(relative, "{outputFolder}");
|
||||
}
|
||||
|
||||
private Path watchedFolderWritingTo(String relative, String outputDir) throws IOException {
|
||||
Path folder = watchedRoot.resolve(relative);
|
||||
Files.createDirectories(folder);
|
||||
Files.writeString(
|
||||
@@ -191,10 +232,11 @@ class WatchedFolderPipelineImportTest {
|
||||
"parameters": {"optimizeLevel": 3, "fileInput": "automated"}
|
||||
}
|
||||
],
|
||||
"outputDir": "{outputFolder}",
|
||||
"outputDir": "%s",
|
||||
"outputFileName": "compressed_{filename}"
|
||||
}
|
||||
""");
|
||||
"""
|
||||
.formatted(outputDir));
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -95,6 +95,21 @@ class DefaultClassificationPolicySeederTest {
|
||||
assertThat(saved.getValue().id()).isEqualTo("p1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsTheProvenanceMarkerWhenClearingThePlaceholderOwner() {
|
||||
when(policyStore.findByTeam(7L))
|
||||
.thenReturn(
|
||||
List.of(
|
||||
classificationPolicy(7L, "system")
|
||||
.withOrigin(Policy.ORIGIN_MIGRATED)));
|
||||
|
||||
seeder().onTeamCreated(new TeamCreatedEvent(7L, "Acme"));
|
||||
|
||||
ArgumentCaptor<Policy> saved = ArgumentCaptor.forClass(Policy.class);
|
||||
verify(policyStore).save(saved.capture());
|
||||
assertThat(saved.getValue().origin()).isEqualTo(Policy.ORIGIN_MIGRATED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void leavesADeliberatelyChosenOwnerAlone() {
|
||||
when(policyStore.findByTeam(7L)).thenReturn(List.of(classificationPolicy(7L, "alice")));
|
||||
|
||||
@@ -97,6 +97,7 @@ public final class SaasSchemaOwnership {
|
||||
"policies",
|
||||
"policy_assets",
|
||||
"policy_completed_migrations",
|
||||
"policy_imported_pipelines",
|
||||
"policy_processed_files",
|
||||
"policy_source_doc_counts",
|
||||
"policy_source_doc_totals",
|
||||
|
||||
Reference in New Issue
Block a user