刘光辉
3 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
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++;
    }
}