Skip to content

Commit cb44b39

Browse files
committed
fix: fail fast on invalid fake-delete config
Initialize the fake-delete access map eagerly to prevent null dereferences. Reject missing or invalid per-table deletedKey values so fake delete cannot silently fall back to physical deletion. issue #654
1 parent c204638 commit cb44b39

3 files changed

Lines changed: 238 additions & 17 deletions

File tree

APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6195,25 +6195,30 @@ else if (w.startsWith("!")) {
61956195

61966196
if (enableFakeDelete && method == DELETE) {
61976197
// 查询 Access 假删除
6198-
Map<String, Object> accessFakeDeleteMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP.get(config.getTable());
6199-
6200-
Object deletedKey = accessFakeDeleteMap.get(KEY_DELETED_KEY);
6201-
if (StringUtil.isNotEmpty(deletedKey, true)) {
6202-
// 假删除需要更新的其他字段,比如:删除时间 deletedTime 之类的
6203-
Map<String, Object> fakeDeleteMap = new HashMap<>();
6204-
fakeDeleteMap.put(deletedKey.toString(), accessFakeDeleteMap.get(KEY_DELETED_VALUE));
6205-
fakeDeleteMap = config.onFakeDelete(fakeDeleteMap);
6206-
6207-
Map<String, Object> content = config.getContent();
6208-
if (content == null || content.isEmpty()) {
6209-
content = fakeDeleteMap;
6210-
} else {
6211-
content.putAll(fakeDeleteMap);
6212-
}
6198+
Map<String, Map<String, Object>> fakeDeleteConfigMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP;
6199+
Map<String, Object> accessFakeDeleteMap = fakeDeleteConfigMap == null
6200+
? null : fakeDeleteConfigMap.get(config.getTable());
62136201

6214-
config.setMethod(PUT);
6215-
config.setContent(content);
6202+
Object deletedKey = accessFakeDeleteMap == null ? null : accessFakeDeleteMap.get(KEY_DELETED_KEY);
6203+
if ((deletedKey instanceof String) == false || StringUtil.isNotEmpty(deletedKey, true) == false) {
6204+
throw new IllegalArgumentException(config.getTable() + " 对应的假删除配置错误!"
6205+
+ KEY_DELETED_KEY + ":value 中 value 必须为非空 String!当前值为 " + deletedKey);
62166206
}
6207+
6208+
// 假删除需要更新的其他字段,比如:删除时间 deletedTime 之类的
6209+
Map<String, Object> fakeDeleteMap = new HashMap<>();
6210+
fakeDeleteMap.put(deletedKey.toString(), accessFakeDeleteMap.get(KEY_DELETED_VALUE));
6211+
fakeDeleteMap = config.onFakeDelete(fakeDeleteMap);
6212+
6213+
Map<String, Object> content = config.getContent();
6214+
if (content == null || content.isEmpty()) {
6215+
content = fakeDeleteMap;
6216+
} else {
6217+
content.putAll(fakeDeleteMap);
6218+
}
6219+
6220+
config.setMethod(PUT);
6221+
config.setContent(content);
62176222
}
62186223

62196224
List<String> cs = new ArrayList<>();

APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ public abstract class AbstractVerifier<T, M extends Map<String, Object>, L exten
177177

178178
ACCESS_MAP = new HashMap<>(SYSTEM_ACCESS_MAP);
179179

180+
ACCESS_FAKE_DELETE_MAP = new LinkedHashMap<>();
181+
180182
REQUEST_MAP = new HashMap<>(ACCESS_MAP.size()*7); // 单个与批量增删改
181183

182184
COMPILE_MAP = new HashMap<String, Pattern>();
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package apijson.orm;
2+
3+
import apijson.JSON;
4+
import apijson.JSONParser;
5+
import apijson.RequestMethod;
6+
import org.junit.After;
7+
import org.junit.AfterClass;
8+
import org.junit.Before;
9+
import org.junit.BeforeClass;
10+
import org.junit.Test;
11+
12+
import java.util.ArrayList;
13+
import java.util.LinkedHashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
import static org.junit.Assert.assertEquals;
18+
import static org.junit.Assert.assertNotNull;
19+
import static org.junit.Assert.assertTrue;
20+
import static org.junit.Assert.fail;
21+
22+
public class AbstractSQLConfigFakeDeleteTest {
23+
private static final String TABLE = "User";
24+
private static final Map<String, Map<String, Object>> EAGERLY_INITIALIZED_MAP =
25+
AbstractVerifier.ACCESS_FAKE_DELETE_MAP;
26+
27+
private static JSONParser<? extends Map<String, Object>, ? extends List<Object>> previousJSONParser;
28+
private Map<String, Map<String, Object>> previousFakeDeleteMap;
29+
30+
@BeforeClass
31+
public static void installJSONParser() {
32+
previousJSONParser = JSON.DEFAULT_JSON_PARSER;
33+
JSON.DEFAULT_JSON_PARSER = new JSONParser<Map<String, Object>, List<Object>>() {
34+
@Override
35+
public Map<String, Object> createJSONObject() {
36+
return new LinkedHashMap<>();
37+
}
38+
39+
@Override
40+
public List<Object> createJSONArray() {
41+
return new ArrayList<>();
42+
}
43+
44+
@Override
45+
public Object parse(Object json) {
46+
return json;
47+
}
48+
49+
@Override
50+
@SuppressWarnings("unchecked")
51+
public Map<String, Object> parseObject(Object json) {
52+
return (Map<String, Object>) json;
53+
}
54+
55+
@Override
56+
public <T> T parseObject(Object json, Class<T> clazz) {
57+
return clazz.cast(json);
58+
}
59+
60+
@Override
61+
@SuppressWarnings("unchecked")
62+
public List<Object> parseArray(Object json) {
63+
return (List<Object>) json;
64+
}
65+
66+
@Override
67+
@SuppressWarnings("unchecked")
68+
public <T> List<T> parseArray(Object json, Class<T> clazz) {
69+
return (List<T>) json;
70+
}
71+
72+
@Override
73+
public String toJSONString(Object obj, boolean format) {
74+
return String.valueOf(obj);
75+
}
76+
};
77+
}
78+
79+
@AfterClass
80+
public static void restoreJSONParser() {
81+
JSON.DEFAULT_JSON_PARSER = previousJSONParser;
82+
}
83+
84+
@Before
85+
public void resetFakeDeleteMap() {
86+
previousFakeDeleteMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP;
87+
AbstractVerifier.ACCESS_FAKE_DELETE_MAP = new LinkedHashMap<>();
88+
}
89+
90+
@After
91+
public void restoreFakeDeleteMap() {
92+
AbstractVerifier.ACCESS_FAKE_DELETE_MAP = previousFakeDeleteMap;
93+
}
94+
95+
@Test
96+
public void eagerlyInitializesFakeDeleteMap() {
97+
assertNotNull(EAGERLY_INITIALIZED_MAP);
98+
}
99+
100+
@Test
101+
public void keepsGetWhenTableHasNoFakeDeleteConfig() throws Exception {
102+
SQLConfig<Long, Map<String, Object>, List<Object>> config = newConfig(RequestMethod.GET, true);
103+
104+
assertEquals(RequestMethod.GET, config.getMethod());
105+
}
106+
107+
@Test
108+
public void rejectsDeleteWhenTableConfigIsMissing() throws Exception {
109+
assertInvalidConfig(null);
110+
}
111+
112+
@Test
113+
public void rejectsDeleteWhenTableConfigIsEmpty() throws Exception {
114+
assertInvalidConfig(new LinkedHashMap<String, Object>());
115+
}
116+
117+
@Test
118+
public void rejectsDeleteWhenDeletedKeyIsBlank() throws Exception {
119+
Map<String, Object> tableConfig = new LinkedHashMap<>();
120+
tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, " ");
121+
122+
assertInvalidConfig(tableConfig);
123+
}
124+
125+
@Test
126+
public void rejectsDeleteWhenDeletedKeyIsNotAString() throws Exception {
127+
Map<String, Object> tableConfig = new LinkedHashMap<>();
128+
tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, 123);
129+
130+
assertInvalidConfig(tableConfig);
131+
}
132+
133+
@Test
134+
public void rewritesConfiguredDeleteAsPut() throws Exception {
135+
Map<String, Object> tableConfig = new LinkedHashMap<>();
136+
tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, "deletedFlag");
137+
tableConfig.put(AbstractSQLConfig.KEY_DELETED_VALUE, 1);
138+
AbstractVerifier.ACCESS_FAKE_DELETE_MAP.put(TABLE, tableConfig);
139+
140+
SQLConfig<Long, Map<String, Object>, List<Object>> config = newConfig(RequestMethod.DELETE, true);
141+
142+
assertEquals(RequestMethod.PUT, config.getMethod());
143+
assertNotNull(config.getContent());
144+
assertEquals(Integer.valueOf(1), config.getContent().get("deletedFlag"));
145+
}
146+
147+
@Test
148+
public void keepsDeleteWhenFakeDeleteIsDisabled() throws Exception {
149+
SQLConfig<Long, Map<String, Object>, List<Object>> config = newConfig(RequestMethod.DELETE, false);
150+
151+
assertEquals(RequestMethod.DELETE, config.getMethod());
152+
}
153+
154+
private static void assertInvalidConfig(Map<String, Object> tableConfig) throws Exception {
155+
if (tableConfig != null) {
156+
AbstractVerifier.ACCESS_FAKE_DELETE_MAP.put(TABLE, tableConfig);
157+
}
158+
159+
try {
160+
SQLConfig<Long, Map<String, Object>, List<Object>> config =
161+
newConfig(RequestMethod.DELETE, true);
162+
fail("Invalid fake-delete config must throw instead of returning " + config.getMethod());
163+
}
164+
catch (IllegalArgumentException e) {
165+
assertNotNull(e.getMessage());
166+
assertTrue(e.getMessage().contains(TABLE));
167+
assertTrue(e.getMessage().contains(AbstractSQLConfig.KEY_DELETED_KEY));
168+
assertTrue(e.getMessage().contains("String"));
169+
}
170+
}
171+
172+
private static SQLConfig<Long, Map<String, Object>, List<Object>> newConfig(
173+
RequestMethod method, final boolean fakeDelete) throws Exception {
174+
Map<String, Object> request = new LinkedHashMap<>();
175+
request.put("id", 1L);
176+
177+
AbstractSQLConfig.Callback<Long, Map<String, Object>, List<Object>> callback =
178+
new AbstractSQLConfig.SimpleCallback<Long, Map<String, Object>, List<Object>>() {
179+
@Override
180+
public SQLConfig<Long, Map<String, Object>, List<Object>> getSQLConfig(RequestMethod method,
181+
String database, String datasource, String namespace, String catalog, String schema,
182+
String table) {
183+
return new AbstractSQLConfig<Long, Map<String, Object>, List<Object>>(method, table) {
184+
@Override
185+
public boolean isFakeDelete() {
186+
return fakeDelete;
187+
}
188+
189+
@Override
190+
public String gainDBVersion() {
191+
return "8.0.0";
192+
}
193+
194+
@Override
195+
public String gainDBUri() {
196+
return "jdbc:mysql://localhost/test";
197+
}
198+
199+
@Override
200+
public String gainDBAccount() {
201+
return "test";
202+
}
203+
204+
@Override
205+
public String gainDBPassword() {
206+
return "test";
207+
}
208+
};
209+
}
210+
};
211+
212+
return AbstractSQLConfig.newSQLConfig(method, TABLE, null, request, null, false, callback);
213+
}
214+
}

0 commit comments

Comments
 (0)