package jnpf.flowable.util; import java.nio.file.*; import java.io.InputStream; import java.util.*; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.session.Configuration; import jnpf.flowable.entity.TemplateNodeEntity; import jnpf.flowable.model.filepermission.*; import jnpf.flowable.model.filepermission.FilePermissionPolicy.*; import jnpf.flowable.service.NativeFilePermissionRuntimeSource; import jnpf.flowable.util.filepermission.*; /** 可在现有 Checks 入口执行;覆盖原生运行事实投影而非只验证规则 JSON。 */ public class FilePermissionRuntimeChecks { private static int checks; public static void main(String[] args) throws Exception { FilePermissionSnapshot snapshot = snapshot("ACTIVE", "CURRENT_HANDLERS", "qa", null); FilePermissionNativeState state = state(1, "qa,review"); state.getMembers().add(member("operator", "qa", "qa-user", 1, 0)); state.getMembers().add(member("operator", "review", "review-user", 1, 0)); FilePermissionRuntime.Facts qa = project(snapshot, state, "qa-user"); check(qa.verified && qa.currentHandlers.get("qa").contains("qa-user"), "native current handlers"); check(!resolve(snapshot, qa).allows("files", Action.DOWNLOAD), "active-node handler denial"); check(resolve(snapshot, project(snapshot, state, "review-user")).allows("files", Action.DOWNLOAD), "parallel node isolation"); check(resolve(snapshot, qa).allows("rows-files", Action.DOWNLOAD), "child field independent"); check(resolve(snapshot, qa).allows("files", Action.EDIT), "current handler native writable"); state.getMembers().get(0).setStartHandleTime(null); FilePermissionRuntime.Facts assigned = project(snapshot, state, "qa-user"); check(assigned.verified && assigned.participants.get("qa").contains("qa-user"), "assigned candidate remains participant"); check(!assigned.currentHandlers.containsKey("qa") && !resolve(snapshot, assigned).allows("files", Action.EDIT), "assigned candidate is not current handler before start"); state.getMembers().get(0).setStartHandleTime(new Date(1L)); check(project(snapshot, state, "qa-user").currentHandlers.get("qa").contains("qa-user"), "started candidate becomes current handler"); check(!resolve(snapshot, project(snapshot, state, "creator")).allows("files", Action.EDIT), "creator not writable during approval"); check(resolve(snapshot, project(snapshot, state, "creator")).allows("files", Action.VIEW), "creator can read start form"); check(!project(snapshot, state, "stranger").verified, "unrelated user not admitted"); qa.transitioning = true; check(!resolve(snapshot, qa).allows("files", Action.VIEW), "mixed state denied"); qa.transitioning = false; qa.nativeUpperBounds.get("files").put(Action.VIEW, false); check(!resolve(snapshot, qa).allows("files", Action.VIEW), "native read restriction retained"); FilePermissionSnapshot completed = snapshot("COMPLETED", "PARTICIPANTS", null, null); state.setStatus(2); state.setCurrentNodeCode(null); FilePermissionView end = resolve(completed, project(completed, state, "qa-user")); check(!end.allows("files", Action.DOWNLOAD) && end.allows("files", Action.VIEW), "terminal download denied and view retained"); check(!end.allows("files", Action.EDIT), "terminal not writable"); FilePermissionSnapshot cancelled = snapshot("CANCELLED", "CREATOR", null, null); state.setStatus(7); check(!resolve(cancelled, project(cancelled, state, "creator")).allows("files", Action.DOWNLOAD), "cancelled trigger"); FilePermissionSnapshot terminated = snapshot("TERMINATED", "CREATOR", null, null); state.setStatus(4); check(!resolve(terminated, project(terminated, state, "creator")).allows("files", Action.DOWNLOAD), "terminated trigger"); FilePermissionSnapshot keep = snapshot("ENTERED", "CREATOR", "qa", "KEEP"); state.setStatus(8); FilePermissionNativeState.Member back = member("record", "qa", "qa-user", -1, null); back.setHandleType(3); state.getMembers().add(back); FilePermissionRuntime.Facts returned = project(keep, state, "creator"); check(returned.verified && returned.activeEnteredRules.contains("rule"), "KEEP retains entered evidence on return"); check(!resolve(keep, returned).allows("files", Action.DOWNLOAD), "KEEP still denies after return"); check(resolve(keep, returned).allows("files", Action.EDIT), "draft/back creator writable"); FilePermissionSnapshot release = snapshot("ENTERED", "CREATOR", "qa", "RELEASE"); release = read(release.withEnteredNodes(Collections.singleton("qa"))); FilePermissionRuntime.Facts activeRelease = project(release, state, "creator"); check(activeRelease.verified && activeRelease.activeEnteredRules.contains("rule"), "RELEASE active after trigger entry"); check(!resolve(release, activeRelease).allows("files", Action.DOWNLOAD), "active RELEASE rule denies action"); release = read(release.withReturnedPastNodes(Collections.singleton("review"))); check(project(release, state, "creator").activeEnteredRules.contains("rule"), "return after trigger retains RELEASE rule"); release = read(release.withReturnedPastNodes(Collections.singleton("qa"))); FilePermissionRuntime.Facts released = project(release, state, "creator"); check(released.verified && !released.activeEnteredRules.contains("rule"), "return before trigger releases RELEASE rule"); check(resolve(release, released).allows("files", Action.DOWNLOAD), "released rule restores inherited action"); release = read(release.withEnteredNodes(Collections.singleton("qa"))); check(project(release, state, "creator").activeEnteredRules.contains("rule"), "trigger re-entry reactivates RELEASE rule"); state.setFlowType(3); check(!project(keep, state, "creator").verified, "unmapped free-flow unavailable"); state.setFlowType(1); state.setStatus(10); check(!project(keep, state, "creator").verified, "exception state unavailable"); state.setStatus(1); state.setCurrentNodeCode("unknown"); check(!project(keep, state, "creator").verified, "unknown active node unavailable"); state.setCurrentNodeCode("qa"); state.setMembers(new ArrayList<>()); check(!project(keep, state, "creator").verified, "current handler not yet persisted unavailable"); state.getMembers().add(member("operator", "qa", "future", -2, 0)); check(!project(keep, state, "future").verified, "future operator not admitted"); state.setStatus(0); FilePermissionRuntime.Facts draft = project(keep, state, "creator"); check(draft.verified && !draft.activeEnteredRules.contains("rule"), "future operator is not entered evidence"); check(resolve(keep, draft).allows("files", Action.DOWNLOAD), "not-yet-entered rule inactive"); List nodes = nodes(); nodes.get(0).setNodeJson("{\"formOperates\":[{\"id\":\"rows\",\"read\":true,\"write\":false}]}"); FilePermissionRuntime.Facts bounds = NativeFilePermissionRuntimeSource.project("0", "form", "creator", keep, state, nodes, form()); check(!resolve(keep, bounds).allows("rows-files", Action.EDIT), "child mutation bounded by parent write"); check(resolve(keep, bounds).allows("files", Action.EDIT), "parent restriction does not affect main file"); nodes.get(0).setNodeJson("{\"formOperates\":[{\"id\":\"files\",\"read\":false,\"write\":true}]}"); bounds = NativeFilePermissionRuntimeSource.project("0", "form", "creator", keep, state, nodes, form()); check(!resolve(keep, bounds).allows("files", Action.EDIT), "write cannot bypass read restriction"); check(!NativeFilePermissionRuntimeSource.project("0", "foreign", "creator", keep, state, nodes, form()).verified, "foreign form not admitted"); Map disabled = form(); ((Map) ((List)disabled.get("fields")).get(0)).put("onlineEdit", false); bounds = NativeFilePermissionRuntimeSource.project("0", "form", "creator", keep, state, nodes(), disabled); check(!resolve(keep, bounds).allows("files", Action.EDIT) && resolve(keep, bounds).allows("files", Action.DOWNLOAD), "online edit switch independent of downloads"); FilePermissionView legacy = FilePermissionView.legacy("task", "form"); check(!legacy.isEnabled() && legacy.allows("files", Action.DOWNLOAD), "legacy defers to original checks"); legacy.setStatus("error"); check(!legacy.allows("files", Action.DOWNLOAD), "malformed legacy response denied"); check(!FilePermissionView.unavailable("task", "form", "offline").allows("files", Action.DOWNLOAD), "unavailable not legacy"); Configuration configuration = new Configuration(); Path xml = Path.of("jnpf-flowable/jnpf-flowable-biz/src/main/resources/mapper/FilePermissionRuntimeMapper.xml"); try (InputStream stream = Files.newInputStream(xml)) { new XMLMapperBuilder(stream, configuration, xml.toString(), configuration.getSqlFragments()).parse(); } org.apache.ibatis.mapping.MappedStatement statement = configuration.getMappedStatement("jnpf.flowable.mapper.FilePermissionRuntimeMapper.find"); String sql = statement.getBoundSql(Map.of("taskId", "task")).getSql(); check(sql.contains("UNION ALL") && statement.getBoundSql(Map.of("taskId", "task")).getParameterMappings().size() == 4, "state SQL binds all four task scopes"); check(sql.contains("f_start_handle_time") && statement.getResultMaps().get(0).getResultMappings().stream() .anyMatch(mapping -> "members".equals(mapping.getProperty()) && mapping.getNestedResultMapId() != null), "state SQL includes start-handle time"); check(statement.getResultMaps().get(0).hasNestedResultMaps(), "explicit native state and member mappings"); System.out.println("FilePermissionRuntimeChecks: " + checks + " passed"); } private static FilePermissionNativeState state(int status, String active) { FilePermissionNativeState state = new FilePermissionNativeState(); state.setTaskId("task"); state.setCreatorId("creator"); state.setFlowId("flow"); state.setFlowType(1); state.setStatus(status); state.setCurrentNodeCode(active); return state; } private static FilePermissionNativeState.Member member(String kind, String node, String user, Integer status, Integer completion) { FilePermissionNativeState.Member m = new FilePermissionNativeState.Member(); m.setId(kind + node + user); m.setKind(kind); m.setNodeCode(node); m.setUserId(user); m.setStatus(status); m.setCompletion(completion); if ("operator".equals(kind) && status != null && status >= 0) m.setStartHandleTime(new Date(1L)); return m; } private static FilePermissionSnapshot snapshot(String trigger, String subject, String node, String onReturn) { Map rule = new HashMap<>(Map.of("id", "rule", "formId", "form", "field", "files", "subject", subject, "trigger", trigger, "actions", Map.of("DOWNLOAD", "DENY"))); if (node != null) rule.put("triggerNodeId", node); if (onReturn != null) rule.put("onReturn", onReturn); return FilePermissionSnapshot.read(FilePermissionSnapshot.create("0", "task", "flow", Map.of("version", 1, "rules", List.of(rule))), "0", "task"); } private static FilePermissionSnapshot read(String json) { return FilePermissionSnapshot.read(json, "0", "task"); } private static List nodes() { List list = new ArrayList<>(); for (String code : List.of("start", "qa", "review")) { TemplateNodeEntity node = new TemplateNodeEntity(); node.setNodeCode(code); node.setNodeType(code.equals("start") ? "start" : "approver"); node.setFormId("form"); node.setNodeJson("{}"); list.add(node); } return list; } private static Map form() { Map upload = new HashMap<>(Map.of("__vModel__", "files", "onlineEdit", true, "__config__", Map.of("jnpfKey", "uploadFile"))); return new HashMap<>(Map.of("fields", List.of(upload, Map.of("__vModel__", "rows", "__config__", Map.of("jnpfKey", "table", "children", List.of(upload)))))); } private static FilePermissionRuntime.Facts project(FilePermissionSnapshot snapshot, FilePermissionNativeState state, String user) { return NativeFilePermissionRuntimeSource.project("0", "form", user, snapshot, state, nodes(), form()); } private static FilePermissionView resolve(FilePermissionSnapshot snapshot, FilePermissionRuntime.Facts facts) { return FilePermissionRuntime.resolve(snapshot.getRules(), facts); } private static void check(boolean value, String label) { if (!value) throw new AssertionError(label); checks++; } }