package jnpf.flowable.util;
|
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import jnpf.flowable.entity.TaskEntity;
|
import jnpf.flowable.mapper.FilePermissionSnapshotMapper;
|
import jnpf.flowable.mapper.TaskMapper;
|
import jnpf.flowable.model.filepermission.FilePermissionPolicy;
|
import jnpf.flowable.model.templatenode.nodejson.NodeModel;
|
import jnpf.flowable.util.filepermission.FilePermissionPolicyCompiler;
|
import jnpf.flowable.util.filepermission.FilePermissionSnapshot;
|
import jnpf.util.JsonUtil;
|
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
import org.apache.ibatis.mapping.MappedStatement;
|
|
import java.io.InputStream;
|
import java.util.*;
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
/** 无数据库写入:验证真实序列化、原生 MyBatis INSERT/UPDATE 及快照隔离契约。 */
|
public final class FilePermissionSnapshotChecks {
|
private static int checks;
|
|
public static void main(String[] args) throws Exception {
|
checkLegacyAndInstances();
|
checkFieldCompilation();
|
checkSupportedConfiguration();
|
checkCorruptSnapshot();
|
checkNativePersistence();
|
System.out.println("FilePermissionSnapshotChecks: " + checks + " checks passed");
|
}
|
|
private static void checkLegacyAndInstances() {
|
AtomicInteger loads = new AtomicInteger();
|
Map<String, Object> empty = FilePermissionPolicyCompiler.compile(nodes(), id -> {
|
loads.incrementAndGet();
|
return form();
|
});
|
require(loads.get() == 0, "legacy nodes must not load forms");
|
require(FilePermissionSnapshot.create("tenant", "old", "v1", empty) == null, "unconfigured new task must stay legacy");
|
require(FilePermissionSnapshot.read(null, "tenant", "old") == null, "old task must remain legacy");
|
|
Map<String, NodeModel> nodes = nodes();
|
Map<String, Object> actions = new LinkedHashMap<>();
|
actions.put("DOWNLOAD", "DENY");
|
nodes.get("review").setFormOperates(Collections.singletonList(field("rows-files", actions)));
|
Map<String, Object> policy = FilePermissionPolicyCompiler.compile(nodes, id -> form());
|
String a = FilePermissionSnapshot.create("tenant", "a", "v1", policy);
|
actions.put("DOWNLOAD", "ALLOW");
|
String b = FilePermissionSnapshot.create("tenant", "b", "v2", FilePermissionPolicyCompiler.compile(nodes, id -> form()));
|
require(effect(a, "a") == FilePermissionPolicy.Effect.DENY, "later template edits must not affect existing snapshot");
|
require(effect(b, "b") == FilePermissionPolicy.Effect.ALLOW, "new task must use its own configuration");
|
require(FilePermissionSnapshot.read(null, "tenant", "old") == null, "new configuration must not enroll old tasks");
|
reject(() -> FilePermissionSnapshot.read(a, "tenant", "b"), "another task cannot borrow a snapshot");
|
reject(() -> FilePermissionSnapshot.read(a, "other-tenant", "a"), "tenant must match");
|
require("v1".equals(FilePermissionSnapshot.read(a, "tenant", "a").getSourceFlowId()), "source version must be fixed");
|
String derived = FilePermissionSnapshot.read(a, "tenant", "a").forDerivedTask("derived");
|
require(effect(derived, "derived") == FilePermissionPolicy.Effect.DENY, "derived task must retain source configuration");
|
require(FilePermissionSnapshot.read(derived, "tenant", "derived").getActiveReleaseRules().isEmpty(), "derived task starts a new release-rule round");
|
reject(() -> FilePermissionSnapshot.read(derived, "tenant", "a"), "derived snapshot has independent identity");
|
require(effect(a, "a") == FilePermissionPolicy.Effect.DENY, "derivation cannot mutate source");
|
}
|
|
private static void checkFieldCompilation() {
|
Map<String, NodeModel> nodes = nodes();
|
nodes.get("review").setFormOperates(Collections.singletonList(field("rows-files", Collections.singletonMap("EDIT", "DENY"))));
|
List<FilePermissionPolicy.Rule> parsed = FilePermissionPolicy.parse(FilePermissionPolicyCompiler.compile(nodes, id -> form()));
|
require(parsed.size() == 1 && "form".equals(parsed.get(0).formId), "approval node inherits start form");
|
require("rows-files".equals(parsed.get(0).field), "child field retains native identity");
|
require(parsed.get(0).subject == FilePermissionPolicy.Subject.CURRENT_HANDLERS, "node field permissions target current handlers");
|
nodes.get("start").setFormOperates(Collections.singletonList(field("files", Collections.singletonMap("ADD", "DENY"))));
|
parsed = FilePermissionPolicy.parse(FilePermissionPolicyCompiler.compile(nodes, id -> form()));
|
require(parsed.stream().anyMatch(r -> r.subject == FilePermissionPolicy.Subject.CREATOR), "start field permissions target creator");
|
|
nodes.get("review").setFormOperates(Collections.singletonList(field("missing", Collections.singletonMap("VIEW", "DENY"))));
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form()), "missing fields must fail");
|
nodes.get("review").setFormOperates(Collections.singletonList(field("title", Collections.singletonMap("VIEW", "DENY"))));
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form()), "text fields cannot pretend to be attachments");
|
nodes.get("review").setFormOperates(Collections.singletonList(field("files", null)));
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form()), "null actions must not downgrade");
|
nodes.get("review").setFormOperates(Collections.emptyList());
|
nodes.get("review").setFilePermissionPolicy(policy("rule", "other-form", "review"));
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form()), "cross-flow form is invalid");
|
nodes.get("review").setFilePermissionPolicy(policy("rule", "form", "removed"));
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form()), "deleted trigger node is invalid");
|
nodes.get("review").setFilePermissionPolicy(policy("rule", "form", "review"));
|
nodes.get("start").setFilePermissionPolicy(policy("rule", "form", "review"));
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form()), "duplicate rule IDs across nodes are invalid");
|
}
|
|
private static void checkSupportedConfiguration() {
|
for (Integer type : Arrays.asList(0, 1, 2, 3, null)) {
|
Map<String, Object> empty = FilePermissionPolicyCompiler.compile(nodes(), id -> {
|
throw new AssertionError("unconfigured flow must not load forms");
|
}, type);
|
require(FilePermissionPolicy.parse(empty).isEmpty(), "unconfigured flow type must retain original behavior");
|
}
|
Map<String, NodeModel> nodes = nodes();
|
Map<String, Object> policy = policy("rule", "form", "review");
|
nodes.get("start").setFilePermissionPolicy(policy);
|
for (int type : Arrays.asList(0, 1)) {
|
require(FilePermissionPolicy.parse(FilePermissionPolicyCompiler.compile(nodes, id -> form(), type)).size() == 1,
|
"supported flow must accept KEEP rules");
|
}
|
for (Integer type : Arrays.asList(2, 3, null, 99)) {
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form(), type), "unsupported configured flow must fail before publication");
|
}
|
@SuppressWarnings("unchecked")
|
Map<String, Object> rule = ((List<Map<String, Object>>) policy.get("rules")).get(0);
|
rule.put("onReturn", "RELEASE");
|
Map<String, Object> releasePolicy = FilePermissionPolicyCompiler.compile(nodes, id -> form(), 0);
|
require(FilePermissionPolicy.parse(releasePolicy).get(0).onReturn == FilePermissionPolicy.OnReturn.RELEASE,
|
"supported flow accepts return release");
|
String releaseJson = FilePermissionSnapshot.create("tenant", "release", "v1", releasePolicy);
|
FilePermissionSnapshot release = FilePermissionSnapshot.read(releaseJson, "tenant", "release");
|
require(release.getActiveReleaseRules().isEmpty(), "release rule starts inactive");
|
release = FilePermissionSnapshot.read(release.withEnteredNodes(Collections.singleton("review")), "tenant", "release");
|
require(release.getActiveReleaseRules().contains("rule"), "entering trigger activates release rule");
|
release = FilePermissionSnapshot.read(release.withReturnedPastNodes(Collections.singleton("later")), "tenant", "release");
|
require(release.getActiveReleaseRules().contains("rule"), "return after trigger keeps release rule active");
|
release = FilePermissionSnapshot.read(release.withReturnedPastNodes(Collections.singleton("review")), "tenant", "release");
|
require(release.getActiveReleaseRules().isEmpty(), "invalidating trigger releases rule");
|
release = FilePermissionSnapshot.read(release.withEnteredNodes(Collections.singleton("review")), "tenant", "release");
|
require(release.getActiveReleaseRules().contains("rule"), "re-entering trigger reactivates release rule");
|
rule.put("onReturn", "KEEP");
|
NodeModel condition = new NodeModel(); condition.setType("condition"); condition.setFormId("condition-form");
|
nodes.put("condition", condition);
|
rule.put("triggerNodeId", "condition");
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form(), 0), "nonparticipant trigger node must fail");
|
rule.put("triggerNodeId", "review");
|
rule.put("subject", "NODE_PARTICIPANTS"); rule.put("subjectNodeId", "condition");
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form(), 0), "nonparticipant subject node must fail");
|
rule.put("subjectNodeId", "review"); rule.put("formId", "condition-form");
|
reject(() -> FilePermissionPolicyCompiler.compile(nodes, id -> form(), 0), "unbound form on nonparticipant node must fail");
|
}
|
|
private static void checkCorruptSnapshot() {
|
reject(() -> FilePermissionSnapshot.read("", "tenant", "a"), "empty string is not legacy");
|
reject(() -> FilePermissionSnapshot.read("null", "tenant", "a"), "JSON null is not legacy");
|
reject(() -> FilePermissionSnapshot.read("{}", "tenant", "a"), "missing version must fail");
|
String valid = FilePermissionSnapshot.create("tenant", "a", "v1", policy("rule", "form", "review"));
|
reject(() -> FilePermissionSnapshot.read(valid + " {}", "tenant", "a"), "trailing tokens must fail");
|
reject(() -> FilePermissionSnapshot.read(valid.replace("\"version\":1", "\"version\":2"), "tenant", "a"), "unknown versions must fail");
|
reject(() -> FilePermissionSnapshot.read(valid.replace("\"taskId\":\"a\"", "\"taskId\":\"b\",\"taskId\":\"a\""), "tenant", "a"), "duplicate properties must fail");
|
reject(() -> FilePermissionSnapshot.read(valid.replace("\"activeReleaseRules\":[]", "\"activeReleaseRules\":[\"unknown\"]"), "tenant", "a"),
|
"unknown active release rule must fail");
|
reject(() -> FilePermissionSnapshot.read("{\"version\":1,\"tenantId\":\"tenant\",\"taskId\":\"a\",\"sourceFlowId\":\"v1\",\"policy\":{\"version\":1,\"rules\":[]}}", "tenant", "a"), "empty controlled snapshot must fail");
|
}
|
|
private static void checkNativePersistence() throws Exception {
|
TaskEntity task = new TaskEntity();
|
task.setId("a");
|
task.setFullName("task");
|
task.setFilePermissionSnapshot("server-snapshot");
|
ObjectMapper json = new ObjectMapper();
|
require(!json.writeValueAsString(task).contains("server-snapshot"), "snapshot must not leak through native JSON");
|
require(json.readValue("{\"id\":\"a\",\"filePermissionSnapshot\":\"forged\"}", TaskEntity.class).getFilePermissionSnapshot() == null,
|
"client JSON cannot set snapshot");
|
require(!JsonUtil.getObjectToString(task).contains("server-snapshot"), "platform serialization must omit snapshot");
|
require(JsonUtil.getJsonToBean("{\"id\":\"a\",\"filePermissionSnapshot\":\"forged\"}", TaskEntity.class).getFilePermissionSnapshot() == null,
|
"platform JSON cannot restore or inject a snapshot");
|
|
MybatisConfiguration configuration = new MybatisConfiguration();
|
configuration.addMapper(TaskMapper.class);
|
String prefix = TaskMapper.class.getName() + ".";
|
String insert = configuration.getMappedStatement(prefix + "insert").getBoundSql(task).getSql().toLowerCase(Locale.ROOT);
|
require(insert.contains("f_file_permission_snapshot"), "snapshot must share the native task INSERT");
|
Map<String, Object> parameters = new HashMap<>();
|
parameters.put("et", task);
|
String update = configuration.getMappedStatement(prefix + "updateById").getBoundSql(parameters).getSql().toLowerCase(Locale.ROOT);
|
require(!update.contains("f_file_permission_snapshot"), "ordinary updates cannot rewrite snapshot");
|
String select = configuration.getMappedStatement(prefix + "selectById").getBoundSql("a").getSql().toLowerCase(Locale.ROOT);
|
require(!select.contains("f_file_permission_snapshot"), "ordinary task reads remain unchanged");
|
task.setFilePermissionSnapshot(null);
|
insert = configuration.getMappedStatement(prefix + "insert").getBoundSql(task).getSql().toLowerCase(Locale.ROOT);
|
require(!insert.contains("f_file_permission_snapshot"), "legacy task INSERT must omit new column");
|
|
String resource = "mapper/FilePermissionSnapshotMapper.xml";
|
try (InputStream stream = FilePermissionSnapshotChecks.class.getClassLoader().getResourceAsStream(resource)) {
|
require(stream != null, "mapper XML must be packaged");
|
new XMLMapperBuilder(stream, configuration, resource, configuration.getSqlFragments()).parse();
|
}
|
MappedStatement statement = configuration.getMappedStatement(FilePermissionSnapshotMapper.class.getName() + ".find");
|
String sql = statement.getBoundSql(Collections.singletonMap("taskId", "a")).getSql().replaceAll("\\s+", " ");
|
require(sql.contains("f_id = ?") && !sql.contains("f_delete_mark IS NULL"), "lookup must distinguish deleted tasks from never-created tasks");
|
require(statement.getResultMaps().get(0).getResultMappings().size() == 3, "snapshot and deletion state need explicit mappings");
|
}
|
|
private static Map<String, NodeModel> nodes() {
|
Map<String, NodeModel> nodes = new LinkedHashMap<>();
|
NodeModel start = new NodeModel(); start.setType("start"); start.setFormId("form");
|
NodeModel review = new NodeModel(); review.setType("approver");
|
nodes.put("start", start); nodes.put("review", review);
|
return nodes;
|
}
|
|
private static Map<String, Object> field(String id, Object actions) {
|
Map<String, Object> field = new LinkedHashMap<>();
|
field.put("id", id); field.put("jnpfKey", "uploadFile"); field.put("filePermissions", actions);
|
return field;
|
}
|
|
private static Map<String, Object> form() {
|
Map<String, Object> childConfig = new HashMap<>();
|
childConfig.put("jnpfKey", "table");
|
childConfig.put("children", Collections.singletonList(component("files", "uploadFile")));
|
Map<String, Object> table = new HashMap<>();
|
table.put("__vModel__", "rows"); table.put("__config__", childConfig);
|
return Collections.singletonMap("fields", Arrays.asList(component("files", "uploadFile"), component("title", "input"), table));
|
}
|
|
private static Map<String, Object> component(String id, String type) {
|
Map<String, Object> field = new HashMap<>();
|
field.put("__vModel__", id); field.put("__config__", Collections.singletonMap("jnpfKey", type));
|
return field;
|
}
|
|
private static Map<String, Object> policy(String id, String formId, String nodeId) {
|
Map<String, Object> rule = new LinkedHashMap<>();
|
rule.put("id", id); rule.put("formId", formId); rule.put("field", "files");
|
rule.put("subject", "PARTICIPANTS"); rule.put("trigger", "ENTERED");
|
rule.put("triggerNodeId", nodeId); rule.put("onReturn", "KEEP");
|
rule.put("actions", Collections.singletonMap("DOWNLOAD", "DENY"));
|
Map<String, Object> policy = new LinkedHashMap<>();
|
policy.put("version", 1); policy.put("rules", Collections.singletonList(rule));
|
return policy;
|
}
|
|
private static FilePermissionPolicy.Effect effect(String snapshot, String taskId) {
|
return FilePermissionSnapshot.read(snapshot, "tenant", taskId).getRules().get(0).actions.get(FilePermissionPolicy.Action.DOWNLOAD);
|
}
|
|
private static void reject(Runnable action, String message) {
|
try { action.run(); } catch (IllegalArgumentException expected) { checks++; return; }
|
throw new AssertionError(message);
|
}
|
|
private static void require(boolean condition, String message) {
|
if (!condition) throw new AssertionError(message);
|
checks++;
|
}
|
}
|