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 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 nodes = nodes(); Map actions = new LinkedHashMap<>(); actions.put("DOWNLOAD", "DENY"); nodes.get("review").setFormOperates(Collections.singletonList(field("rows-files", actions))); Map 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 nodes = nodes(); nodes.get("review").setFormOperates(Collections.singletonList(field("rows-files", Collections.singletonMap("EDIT", "DENY")))); List 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 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 nodes = nodes(); Map 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 rule = ((List>) policy.get("rules")).get(0); rule.put("onReturn", "RELEASE"); Map 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 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 nodes() { Map 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 field(String id, Object actions) { Map field = new LinkedHashMap<>(); field.put("id", id); field.put("jnpfKey", "uploadFile"); field.put("filePermissions", actions); return field; } private static Map form() { Map childConfig = new HashMap<>(); childConfig.put("jnpfKey", "table"); childConfig.put("children", Collections.singletonList(component("files", "uploadFile"))); Map 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 component(String id, String type) { Map field = new HashMap<>(); field.put("__vModel__", id); field.put("__config__", Collections.singletonMap("jnpfKey", type)); return field; } private static Map policy(String id, String formId, String nodeId) { Map 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 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++; } }