diff --git a/agent-operator/pom.xml b/agent-operator/pom.xml index 3f45888696..4c085e0d4f 100644 --- a/agent-operator/pom.xml +++ b/agent-operator/pom.xml @@ -39,13 +39,16 @@ org.bouncycastle - bcprov-ext-jdk18on + bcprov-jdk18on org.bouncycastle bcpkix-jdk18on - + + org.bouncycastle + bcutil-jdk18on + com.fasterxml.jackson.core jackson-core diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessResource.java b/server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessResource.java index 98725d417f..2e1a7a5031 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessResource.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/process/ProcessResource.java @@ -747,15 +747,17 @@ public void appendLog(@PathParam("id") UUID instanceId, InputStream data) { content = @Content(mediaType = "application/zip", schema = @Schema(type = "string", format = "binary")) ) - public Response downloadState(@PathParam("id") UUID instanceId) { + public Response downloadState(@PathParam("id") UUID instanceId, @Context HttpServletRequest request) { ProcessEntry entry = assertProcess(PartialProcessKey.from(instanceId)); ProcessKey processKey = new ProcessKey(entry.instanceId(), entry.createdAt()); assertProcessAccess(entry, "state"); + boolean applyFilter = assertApplyFilter(request); + StreamingOutput out = output -> { try (ZipArchiveOutputStream dst = new ZipArchiveOutputStream(output)) { - stateManager.export(processKey, new ProcessStateManager.FilteringConsumer(zipTo(dst), s -> { + stateManager.export(processKey, new ProcessStateManager.FilteringConsumer(zipTo(dst, applyFilter), s -> { if (!isSessionResource(s)) { return true; } @@ -769,6 +771,21 @@ public Response downloadState(@PathParam("id") UUID instanceId) { .build(); } + private boolean assertApplyFilter(HttpServletRequest request) { + // do not filter request from admins + if (Roles.isAdmin()) { + return false; + } + + // do not filter request from agent + String userAgent = request.getHeader("User-Agent"); + if (userAgent.startsWith("Concord-Agent")) { + return false; + } + + return true; + } + /** * Downloads a single file from the current state snapshot of a process. */ diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/process/StateManagerUtils.java b/server/impl/src/main/java/com/walmartlabs/concord/server/process/StateManagerUtils.java new file mode 100644 index 0000000000..5fce5484cb --- /dev/null +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/process/StateManagerUtils.java @@ -0,0 +1,67 @@ +package com.walmartlabs.concord.server.process; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.walmartlabs.concord.sdk.Constants.Files.CONFIGURATION_FILE_NAME; + +public final class StateManagerUtils { + + private static final Map> STATE_FILTER = Map.of(CONFIGURATION_FILE_NAME, List.of("arguments")); + private static final List ALLOWED_EXTENSIONS = List.of("json", "yaml", "yml"); + + public static InputStream stateFilter(String file, InputStream in) { + try { + String extension = getFileExtension(file); + if (!ALLOWED_EXTENSIONS.contains(extension) || STATE_FILTER.get(file) == null) { + // only filter for allowed extension files + return in; + } + + byte[] inputBytes = in.readAllBytes(); + if (inputBytes.length == 0) { + return new ByteArrayInputStream(inputBytes); + } + + Map map = switch (extension) { + case "json" -> new ObjectMapper().readValue(inputBytes, Map.class); + case "yaml", "yml" -> new ObjectMapper(new YAMLFactory()).readValue(inputBytes, Map.class); + default -> null; + }; + + if (map == null) { + return new ByteArrayInputStream(inputBytes); + } + + Map filteredMap = STATE_FILTER.get(file).stream() + .filter(map::containsKey) + .collect(HashMap::new, (m, key) -> m.put(key, map.get(key)), HashMap::putAll); + + byte[] data = switch (extension) { + case "json" -> new ObjectMapper().writeValueAsBytes(filteredMap); + case "yaml", "yml" -> new ObjectMapper(new YAMLFactory()).writeValueAsBytes(filteredMap); + default -> throw new IllegalArgumentException("Unsupported file extension: " + extension); + }; + + return new ByteArrayInputStream(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static String getFileExtension(String fileName) { + if (fileName.lastIndexOf(".") != -1 && fileName.lastIndexOf(".") != 0) + return fileName.substring(fileName.lastIndexOf(".") + 1); + else return ""; + } + + private StateManagerUtils() { + } +} diff --git a/server/impl/src/main/java/com/walmartlabs/concord/server/process/state/ProcessStateManager.java b/server/impl/src/main/java/com/walmartlabs/concord/server/process/state/ProcessStateManager.java index 61892e3013..051d9be3c6 100644 --- a/server/impl/src/main/java/com/walmartlabs/concord/server/process/state/ProcessStateManager.java +++ b/server/impl/src/main/java/com/walmartlabs/concord/server/process/state/ProcessStateManager.java @@ -34,6 +34,7 @@ import com.walmartlabs.concord.server.cfg.SecretStoreConfiguration; import com.walmartlabs.concord.server.policy.PolicyException; import com.walmartlabs.concord.server.policy.PolicyManager; +import com.walmartlabs.concord.server.process.StateManagerUtils; import com.walmartlabs.concord.server.process.logs.ProcessLogManager; import com.walmartlabs.concord.server.sdk.PartialProcessKey; import com.walmartlabs.concord.server.sdk.ProcessKey; @@ -510,8 +511,8 @@ public static ItemConsumer copyTo(Path dst, String[] ignored, OpenOption... opti * * @param dst archive stream. */ - public static ItemConsumer zipTo(ZipArchiveOutputStream dst) { - return new ZipConsumer(dst); + public static ItemConsumer zipTo(ZipArchiveOutputStream dst, boolean filterContents) { + return new ZipConsumer(dst, filterContents); } public static ItemConsumer exclude(ItemConsumer delegate, String... patterns) { @@ -792,19 +793,22 @@ public void accept(String name, int unixMode, InputStream src) { public static final class ZipConsumer implements ItemConsumer { private final ZipArchiveOutputStream dst; + private final boolean filterContents; - private ZipConsumer(ZipArchiveOutputStream dst) { + private ZipConsumer(ZipArchiveOutputStream dst, boolean filterContents) { this.dst = dst; + this.filterContents = filterContents; } @Override public void accept(String name, int unixMode, InputStream src) { ZipArchiveEntry entry = new ZipArchiveEntry(name); entry.setUnixMode(unixMode); - + // filter before zip to download + InputStream processed = filterContents ? StateManagerUtils.stateFilter(name, src) : src; try { dst.putArchiveEntry(entry); - IOUtils.copy(src, dst); + IOUtils.copy(processed, dst); dst.closeArchiveEntry(); } catch (IOException e) { throw new RuntimeException(e); diff --git a/server/impl/src/test/java/com/walmartlabs/concord/server/process/StateManagerUtilsTest.java b/server/impl/src/test/java/com/walmartlabs/concord/server/process/StateManagerUtilsTest.java new file mode 100644 index 0000000000..ad1ec3a14d --- /dev/null +++ b/server/impl/src/test/java/com/walmartlabs/concord/server/process/StateManagerUtilsTest.java @@ -0,0 +1,90 @@ +package com.walmartlabs.concord.server.process; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class StateManagerUtilsTest { + + @Test + void testFilterWithJsonFile() throws Exception { + String jsonInput = "{\"arguments\": \"value\", \"otherKey\": \"otherValue\"}"; + InputStream inputStream = new ByteArrayInputStream(jsonInput.getBytes(StandardCharsets.UTF_8)); + + InputStream result = StateManagerUtils.stateFilter("_main.json", inputStream); + String text = new String(result.readAllBytes(), StandardCharsets.UTF_8); + + ObjectMapper mapper = new ObjectMapper(); + Map resultMap = mapper.readValue(text, Map.class); + + assertEquals(1, resultMap.size()); + assertEquals("value", resultMap.get("arguments")); + } + + @Test + void testFilterWithYamlFile() throws Exception { + String yamlInput = "arguments: value\notherKey: otherValue"; + InputStream inputStream = new ByteArrayInputStream(yamlInput.getBytes(StandardCharsets.UTF_8)); + + InputStream result = StateManagerUtils.stateFilter("test.yaml", inputStream); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + Map resultMap = mapper.readValue(result, Map.class); + + // return the same if to stateFilter items not present + assertEquals(2, resultMap.size()); + assertEquals("otherValue", resultMap.get("otherKey")); + } + + @Test + void testFilterWithUnsupportedExtension() throws Exception { + String input = "key: value"; + InputStream inputStream = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); + + InputStream result = StateManagerUtils.stateFilter("file.txt", inputStream); + String text = new String(result.readAllBytes(), StandardCharsets.UTF_8); + + assertEquals(input, text); + } + + // Test case for empty input + @Test + void testFilterWithEmptyInput() throws Exception { + String input = ""; + InputStream inputStream = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); + + InputStream result = StateManagerUtils.stateFilter("_main.json", inputStream); + String text = new String(result.readAllBytes(), StandardCharsets.UTF_8); + + assertEquals(input, text); + } + + // Test case for null filtering rules + @Test + void testFilterWithNullRules() throws Exception { + String jsonInput = "{\"key\": \"value\"}"; + InputStream inputStream = new ByteArrayInputStream(jsonInput.getBytes(StandardCharsets.UTF_8)); + + InputStream result = StateManagerUtils.stateFilter("unknown.json", inputStream); + String text = new String(result.readAllBytes(), StandardCharsets.UTF_8); + + assertEquals(jsonInput, text); + } + + // Test case for null input + @Test + void testFilterWithNullInput() throws Exception { + String jsonInput = "{\"key\": \"value\"}"; + InputStream inputStream = null; + InputStream result = StateManagerUtils.stateFilter("unknown.json", inputStream); + + assertNull(result); + } +} diff --git a/targetplatform/pom.xml b/targetplatform/pom.xml index ca9d447522..bacee5c0f9 100644 --- a/targetplatform/pom.xml +++ b/targetplatform/pom.xml @@ -39,7 +39,6 @@ 1.0 1.11.475 1.80 - 1.78.1 1.0.3 1.14.9 1.9.4 @@ -1103,17 +1102,17 @@ org.bouncycastle - bcprov-ext-jdk18on - ${bouncycastle.ext.version} + bcprov-jdk18on + ${bouncycastle.version} org.bouncycastle - bcprov-jdk18on + bcpkix-jdk18on ${bouncycastle.version} org.bouncycastle - bcpkix-jdk18on + bcutil-jdk18on ${bouncycastle.version}