feat: M28 增加工作流汇聚安全校验

This commit is contained in:
2026-08-31 15:54:50 +08:00
parent 38078741f2
commit e40bd9dc82
2 changed files with 329 additions and 0 deletions

View File

@@ -59,6 +59,8 @@ public class WorkflowCheckService {
private static final String SYSTEM_START_PARAM_NAME = "user_input"; private static final String SYSTEM_START_PARAM_NAME = "user_input";
private static final int MIN_LOOP_COUNT = 1; private static final int MIN_LOOP_COUNT = 1;
private static final int MAX_LOOP_COUNT = 300; private static final int MAX_LOOP_COUNT = 300;
private static final String JOIN_MODE_ANY = "any";
private static final String JOIN_MODE_ALL = "all";
@Resource @Resource
private WorkflowService workflowService; private WorkflowService workflowService;
@@ -196,6 +198,10 @@ public class WorkflowCheckService {
edge.id = trimToNull(edgeJson.getString("id")); edge.id = trimToNull(edgeJson.getString("id"));
edge.source = trimToNull(edgeJson.getString("source")); edge.source = trimToNull(edgeJson.getString("source"));
edge.target = trimToNull(edgeJson.getString("target")); edge.target = trimToNull(edgeJson.getString("target"));
JSONObject edgeData = edgeJson.getJSONObject("data");
edge.condition = edgeData == null
? null
: trimToNull(edgeData.getString("condition"));
if (!StringUtils.hasText(edge.id)) { if (!StringUtils.hasText(edge.id)) {
addIssue(issues, issueKeys, "EDGE_ID_EMPTY", "存在连线缺少 id", null, null, null); addIssue(issues, issueKeys, "EDGE_ID_EMPTY", "存在连线缺少 id", null, null, null);
@@ -228,10 +234,162 @@ public class WorkflowCheckService {
parsedWorkflow.nodes = nodes; parsedWorkflow.nodes = nodes;
parsedWorkflow.edges = edges; parsedWorkflow.edges = edges;
parsedWorkflow.nodeMap = nodeMap; parsedWorkflow.nodeMap = nodeMap;
checkJoinModes(parsedWorkflow, issues, issueKeys);
checkDatacenterNodes(parsedWorkflow, issues, issueKeys); checkDatacenterNodes(parsedWorkflow, issues, issueKeys);
return parsedWorkflow; return parsedWorkflow;
} }
/**
* 校验节点汇聚模式及其静态可证明的到达安全性。
*
* @param parsed 工作流视图
* @param issues 问题列表
* @param issueKeys 问题去重键
*/
private void checkJoinModes(
ParsedWorkflow parsed,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
Map<String, List<EdgeView>> inwardEdges = new LinkedHashMap<>();
for (EdgeView edge : parsed.edges) {
if (edge == null || !StringUtils.hasText(edge.target)) {
continue;
}
inwardEdges.computeIfAbsent(
edge.target, ignored -> new ArrayList<>()).add(edge);
}
for (NodeView node : parsed.nodes) {
String joinMode = resolveJoinMode(node);
if (joinMode == null) {
addIssue(
issues,
issueKeys,
"JOIN_MODE_INVALID",
"执行时机配置无效joinMode 仅支持 any 或 all",
node.id,
null,
node.name);
continue;
}
if (JOIN_MODE_ALL.equals(joinMode)
&& StringUtils.hasText(node.parentId)) {
addIssue(
issues,
issueKeys,
"JOIN_MODE_LOOP_CHILD_UNSUPPORTED",
"显式循环子图暂不支持“全部上游完成”,请改为“任一上游完成”",
node.id,
null,
node.name);
}
}
Set<String> guaranteedNodes = findGuaranteedNodes(
parsed, inwardEdges);
for (NodeView node : parsed.nodes) {
if (!JOIN_MODE_ALL.equals(resolveJoinMode(node))
|| StringUtils.hasText(node.parentId)) {
continue;
}
List<EdgeView> directInward = inwardEdges.getOrDefault(
node.id, Collections.emptyList());
if (directInward.size() <= 1) {
continue;
}
boolean allGuaranteed = directInward.stream().allMatch(edge ->
!edge.hasCondition()
&& guaranteedNodes.contains(edge.source));
if (!allGuaranteed) {
addIssue(
issues,
issueKeys,
"JOIN_MODE_CONDITIONAL_PATH_UNSUPPORTED",
"“全部上游完成”可能永久等待:存在条件、互斥或无法证明必达的上游路径。"
+ "请改为“任一上游完成”或调整连线,确保所有直接入边都会到达",
node.id,
null,
node.name);
}
}
}
/**
* 使用保守固定点传播计算能够保证执行的根级节点。
*
* @param parsed 工作流视图
* @param inwardEdges 直接入边索引
* @return 保证执行的节点 ID
*/
private Set<String> findGuaranteedNodes(
ParsedWorkflow parsed,
Map<String, List<EdgeView>> inwardEdges) {
Set<String> guaranteed = parsed.nodes.stream()
.filter(NodeView::isRootLevel)
.filter(node -> TYPE_START.equals(node.type))
.map(node -> node.id)
.filter(StringUtils::hasText)
.collect(Collectors.toCollection(LinkedHashSet::new));
boolean changed;
do {
changed = false;
for (NodeView node : parsed.nodes) {
if (!node.isRootLevel()
|| guaranteed.contains(node.id)
|| hasAdvancedCondition(node)) {
continue;
}
String joinMode = resolveJoinMode(node);
if (joinMode == null) {
continue;
}
List<EdgeView> directInward = inwardEdges.getOrDefault(
node.id, Collections.emptyList());
boolean isGuaranteed;
if (JOIN_MODE_ALL.equals(joinMode)) {
isGuaranteed = !directInward.isEmpty()
&& directInward.stream().allMatch(edge ->
!edge.hasCondition()
&& guaranteed.contains(edge.source));
} else {
isGuaranteed = directInward.stream().anyMatch(edge ->
!edge.hasCondition()
&& guaranteed.contains(edge.source));
}
if (isGuaranteed && guaranteed.add(node.id)) {
changed = true;
}
}
} while (changed);
return guaranteed;
}
/**
* 读取节点汇聚模式。字段缺失时兼容为 any显式非法值返回 null。
*/
private String resolveJoinMode(NodeView node) {
if (node == null || node.data == null
|| !node.data.containsKey("joinMode")) {
return JOIN_MODE_ANY;
}
String value = trimToNull(node.data.getString("joinMode"));
if (JOIN_MODE_ANY.equalsIgnoreCase(value)) {
return JOIN_MODE_ANY;
}
if (JOIN_MODE_ALL.equalsIgnoreCase(value)) {
return JOIN_MODE_ALL;
}
return null;
}
private boolean hasAdvancedCondition(NodeView node) {
return node != null
&& node.data != null
&& StringUtils.hasText(
trimToNull(node.data.getString("condition")));
}
/** /**
* 校验普通循环、显式循环和循环父子层级。 * 校验普通循环、显式循环和循环父子层级。
* *
@@ -1610,5 +1768,10 @@ public class WorkflowCheckService {
private String id; private String id;
private String source; private String source;
private String target; private String target;
private String condition;
private boolean hasCondition() {
return StringUtils.hasText(condition);
}
} }
} }

View File

@@ -22,6 +22,163 @@ import java.util.Map;
public class WorkflowCheckServiceTest { public class WorkflowCheckServiceTest {
@Test
public void testSaveAndPreExecuteShouldPassGuaranteedAllJoin() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject joinData = data("汇聚");
joinData.put("joinMode", "all");
String content = workflowJson(
array(
node("start", "startNode", null, data("开始")),
node("a", "codeNode", null, data("分支 A")),
node("b", "codeNode", null, data("分支 B")),
node("join", "codeNode", null, joinData),
node("end", "endNode", null, data("结束"))),
array(
edge("start-a", "start", "a"),
edge("start-b", "start", "b"),
edge("a-join", "a", "join"),
edge("b-join", "b", "join"),
edge("join-end", "join", "end")));
Assert.assertTrue(service.checkContent(
content, WorkflowCheckStage.SAVE, null).isPassed());
Assert.assertTrue(service.checkContent(
content, WorkflowCheckStage.PRE_EXECUTE, null).isPassed());
}
@Test
public void testSaveAndPreExecuteShouldBlockConditionalAllJoin() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject joinData = data("汇聚");
joinData.put("joinMode", "all");
String content = workflowJson(
array(
node("start", "startNode", null, data("开始")),
node("a", "codeNode", null, data("条件来源")),
node("b", "codeNode", null, data("普通来源")),
node("join", "codeNode", null, joinData),
node("end", "endNode", null, data("结束"))),
array(
conditionalEdge("start-a", "start", "a", "enabled === true"),
edge("start-b", "start", "b"),
edge("a-join", "a", "join"),
edge("b-join", "b", "join"),
edge("join-end", "join", "end")));
WorkflowCheckResult save = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
WorkflowCheckResult preExecute = service.checkContent(
content, WorkflowCheckStage.PRE_EXECUTE, null);
Assert.assertFalse(save.isPassed());
Assert.assertFalse(preExecute.isPassed());
assertHasCode(save, "JOIN_MODE_CONDITIONAL_PATH_UNSUPPORTED");
assertHasCode(preExecute, "JOIN_MODE_CONDITIONAL_PATH_UNSUPPORTED");
Assert.assertTrue(save.getIssues().stream().anyMatch(issue ->
"join".equals(issue.getNodeId())
&& issue.getMessage().contains("永久等待")
&& issue.getMessage().contains("任一上游完成")));
}
@Test
public void testSaveShouldBlockAllJoinWithDirectConditionalEdge() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject joinData = data("汇聚");
joinData.put("joinMode", "all");
String content = workflowJson(
array(
node("start", "startNode", null, data("开始")),
node("a", "codeNode", null, data("分支 A")),
node("b", "codeNode", null, data("分支 B")),
node("join", "codeNode", null, joinData)),
array(
edge("start-a", "start", "a"),
edge("start-b", "start", "b"),
conditionalEdge("a-join", "a", "join", "matched === true"),
edge("b-join", "b", "join")));
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "JOIN_MODE_CONDITIONAL_PATH_UNSUPPORTED");
}
@Test
public void testSaveShouldBlockAllJoinFromCustomConditionSource() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject conditionalSource = data("高级条件来源");
conditionalSource.put("condition", "score > 0");
JSONObject joinData = data("汇聚");
joinData.put("joinMode", "all");
String content = workflowJson(
array(
node("start", "startNode", null, data("开始")),
node("a", "codeNode", null, conditionalSource),
node("b", "codeNode", null, data("普通来源")),
node("join", "codeNode", null, joinData)),
array(
edge("start-a", "start", "a"),
edge("start-b", "start", "b"),
edge("a-join", "a", "join"),
edge("b-join", "b", "join")));
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "JOIN_MODE_CONDITIONAL_PATH_UNSUPPORTED");
}
@Test
public void testSaveShouldBlockInvalidAndLoopChildJoinModes() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject invalidData = data("非法汇聚");
invalidData.put("joinMode", "first");
JSONObject loopData = loopData(
fixedParameter("count", "2", "Number"), null);
JSONObject childData = data("循环子节点");
childData.put("joinMode", "all");
String content = workflowJson(
array(
node("invalid", "codeNode", null, invalidData),
node("loop", "loopNode", null, loopData),
node("child", "codeNode", "loop", childData)),
new JSONArray());
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "JOIN_MODE_INVALID");
assertHasCode(result, "JOIN_MODE_LOOP_CHILD_UNSUPPORTED");
Assert.assertTrue(result.getIssues().stream().anyMatch(issue ->
"invalid".equals(issue.getNodeId())
&& "JOIN_MODE_INVALID".equals(issue.getCode())));
Assert.assertTrue(result.getIssues().stream().anyMatch(issue ->
"child".equals(issue.getNodeId())
&& "JOIN_MODE_LOOP_CHILD_UNSUPPORTED".equals(issue.getCode())));
}
@Test
public void testSaveShouldAllowSingleConditionalInboundAllJoin() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject joinData = data("单入边汇聚");
joinData.put("joinMode", "all");
String content = workflowJson(
array(
node("start", "startNode", null, data("开始")),
node("join", "codeNode", null, joinData)),
array(conditionalEdge(
"start-join", "start", "join", "enabled === true")));
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertTrue(result.isPassed());
}
/** /**
* 验证保存阶段接受合法的正则条件规则。 * 验证保存阶段接受合法的正则条件规则。
*/ */
@@ -992,4 +1149,13 @@ public class WorkflowCheckServiceTest {
edge.put("target", target); edge.put("target", target);
return edge; return edge;
} }
private static JSONObject conditionalEdge(
String id, String source, String target, String condition) {
JSONObject edge = edge(id, source, target);
JSONObject data = new JSONObject();
data.put("condition", condition);
edge.put("data", data);
return edge;
}
} }