1use crate::core::ModelAdmin;
14use crate::types::AdminError;
15use std::collections::HashMap;
16
17pub(crate) fn retain_allowed_fields<T: AsRef<str>>(
18 data: &mut HashMap<String, serde_json::Value>,
19 allowed_fields: &[T],
20) {
21 data.retain(|field, _| {
22 allowed_fields
23 .iter()
24 .any(|allowed| field == allowed.as_ref())
25 });
26}
27
28const MAX_FIELDS: usize = 100;
30
31const MAX_STRING_LENGTH: usize = 1_000_000; const MAX_PAYLOAD_SIZE: usize = 10_000_000; pub fn validate_mutation_data(
65 data: &HashMap<String, serde_json::Value>,
66 model_admin: &dyn ModelAdmin,
67 is_update: bool,
68) -> Result<(), AdminError> {
69 validate_field_count(data)?;
71
72 validate_payload_size(data)?;
74
75 let allowed_fields = get_allowed_fields(model_admin);
77 let readonly_fields: Vec<&str> = model_admin.readonly_fields();
78 let pk_field = model_admin.pk_field();
79
80 for (field_name, value) in data {
82 validate_field_allowed(field_name, &allowed_fields)?;
84
85 if readonly_fields.contains(&field_name.as_str()) {
87 return Err(AdminError::ValidationError(format!(
88 "Field '{}' is read-only and cannot be modified",
89 field_name
90 )));
91 }
92
93 if is_update && field_name == pk_field {
97 return Err(AdminError::ValidationError(format!(
98 "Primary key field '{}' cannot be modified",
99 field_name
100 )));
101 }
102
103 validate_value_size(field_name, value)?;
105 }
106
107 Ok(())
108}
109
110fn get_allowed_fields(model_admin: &dyn ModelAdmin) -> Vec<&str> {
114 model_admin
115 .fields()
116 .unwrap_or_else(|| model_admin.list_display())
117}
118
119fn validate_field_count(data: &HashMap<String, serde_json::Value>) -> Result<(), AdminError> {
121 if data.len() > MAX_FIELDS {
122 return Err(AdminError::ValidationError(format!(
123 "Too many fields in request: {} (max {})",
124 data.len(),
125 MAX_FIELDS
126 )));
127 }
128 Ok(())
129}
130
131fn validate_payload_size(data: &HashMap<String, serde_json::Value>) -> Result<(), AdminError> {
133 let total_size: usize = data
134 .iter()
135 .map(|(k, v)| k.len() + v.to_string().len())
136 .sum();
137
138 if total_size > MAX_PAYLOAD_SIZE {
139 return Err(AdminError::ValidationError(format!(
140 "Payload too large: {} bytes (max {} bytes)",
141 total_size, MAX_PAYLOAD_SIZE
142 )));
143 }
144 Ok(())
145}
146
147fn validate_field_allowed(field_name: &str, allowed_fields: &[&str]) -> Result<(), AdminError> {
149 if !allowed_fields.contains(&field_name) {
150 return Err(AdminError::ValidationError(format!(
151 "Field '{}' is not allowed. Allowed fields: {:?}",
152 field_name, allowed_fields
153 )));
154 }
155 Ok(())
156}
157
158fn validate_value_size(field_name: &str, value: &serde_json::Value) -> Result<(), AdminError> {
160 match value {
161 serde_json::Value::String(s) => {
162 if s.len() > MAX_STRING_LENGTH {
163 return Err(AdminError::ValidationError(format!(
164 "Field '{}' value too long: {} bytes (max {} bytes)",
165 field_name,
166 s.len(),
167 MAX_STRING_LENGTH
168 )));
169 }
170 }
171 serde_json::Value::Array(arr) if arr.len() > MAX_FIELDS => {
172 return Err(AdminError::ValidationError(format!(
173 "Field '{}' array too large: {} elements (max {})",
174 field_name,
175 arr.len(),
176 MAX_FIELDS
177 )));
178 }
179 serde_json::Value::Object(obj) if obj.len() > MAX_FIELDS => {
180 return Err(AdminError::ValidationError(format!(
181 "Field '{}' object too large: {} keys (max {})",
182 field_name,
183 obj.len(),
184 MAX_FIELDS
185 )));
186 }
187 _ => {}
188 }
189 Ok(())
190}
191
192#[cfg(all(test, server))]
193mod tests {
194 use super::*;
195 use crate::core::ModelAdminConfig;
196 use rstest::rstest;
197
198 fn create_test_admin() -> ModelAdminConfig {
199 ModelAdminConfig::builder()
200 .model_name("TestModel")
201 .list_display(vec!["id", "name", "email", "created_at"])
202 .fields(vec!["id", "name", "email", "created_at"])
203 .readonly_fields(vec!["created_at"])
204 .build()
205 .unwrap()
206 }
207
208 #[rstest]
209 fn test_validate_empty_data() {
210 let admin = create_test_admin();
211 let data = HashMap::new();
212 assert!(validate_mutation_data(&data, &admin, false).is_ok());
213 }
214
215 #[rstest]
216 fn test_validate_allowed_field() {
217 let admin = create_test_admin();
218 let mut data = HashMap::new();
219 data.insert("name".to_string(), serde_json::json!("Alice"));
220
221 assert!(validate_mutation_data(&data, &admin, false).is_ok());
222 }
223
224 #[rstest]
225 fn retain_allowed_fields_removes_unconfigured_columns() {
226 let mut data = HashMap::from([
227 ("id".to_string(), serde_json::json!(1)),
228 ("name".to_string(), serde_json::json!("visible")),
229 ("Name".to_string(), serde_json::json!("hidden")),
230 ("reset_token".to_string(), serde_json::json!("secret")),
231 ]);
232
233 retain_allowed_fields(&mut data, &["name"]);
234
235 assert_eq!(
236 data,
237 HashMap::from([("name".to_string(), serde_json::json!("visible"))])
238 );
239 }
240
241 #[rstest]
242 fn test_validate_disallowed_field() {
243 let admin = create_test_admin();
244 let mut data = HashMap::new();
245 data.insert("hacked".to_string(), serde_json::json!("value"));
246
247 let result = validate_mutation_data(&data, &admin, false);
248 assert!(result.is_err());
249 assert!(matches!(
250 result.unwrap_err(),
251 AdminError::ValidationError(_)
252 ));
253 }
254
255 #[rstest]
256 fn test_validate_readonly_field() {
257 let admin = create_test_admin();
258 let mut data = HashMap::new();
259 data.insert("created_at".to_string(), serde_json::json!("2024-01-01"));
260
261 let result = validate_mutation_data(&data, &admin, false);
262 assert!(result.is_err());
263 let err = result.unwrap_err();
264 assert!(matches!(err, AdminError::ValidationError(_)));
265 assert!(err.to_string().contains("read-only"));
266 }
267
268 #[rstest]
269 fn test_validate_pk_field_on_update() {
270 let admin = create_test_admin();
271 let mut data = HashMap::new();
272 data.insert("id".to_string(), serde_json::json!(999));
273
274 let result = validate_mutation_data(&data, &admin, true);
275 assert!(result.is_err());
276 let err = result.unwrap_err();
277 assert!(matches!(err, AdminError::ValidationError(_)));
278 assert!(err.to_string().contains("Primary key"));
279 }
280
281 #[rstest]
282 fn test_validate_pk_field_on_create() {
283 let admin = create_test_admin();
285 let mut data = HashMap::new();
286 data.insert("id".to_string(), serde_json::json!(999));
287
288 let result = validate_mutation_data(&data, &admin, false);
289 assert!(result.is_ok());
290 }
291
292 #[rstest]
293 fn test_validate_too_many_fields() {
294 let admin = create_test_admin();
295 let mut data = HashMap::new();
296
297 for i in 0..=MAX_FIELDS {
299 data.insert(format!("name_{}", i), serde_json::json!("value"));
300 }
301
302 let result = validate_mutation_data(&data, &admin, false);
303 assert!(result.is_err());
304 let err = result.unwrap_err();
305 assert!(matches!(err, AdminError::ValidationError(_)));
306 assert!(err.to_string().contains("Too many fields"));
307 }
308
309 #[rstest]
310 fn test_validate_string_too_long() {
311 let admin = create_test_admin();
312 let mut data = HashMap::new();
313 data.insert(
314 "name".to_string(),
315 serde_json::json!("x".repeat(MAX_STRING_LENGTH + 1)),
316 );
317
318 let result = validate_mutation_data(&data, &admin, false);
319 assert!(result.is_err());
320 let err = result.unwrap_err();
321 assert!(matches!(err, AdminError::ValidationError(_)));
322 assert!(err.to_string().contains("too long"));
323 }
324
325 #[rstest]
326 fn test_validate_array_too_large() {
327 let admin = create_test_admin();
328 let mut data = HashMap::new();
329 let large_array: Vec<_> = (0..=MAX_FIELDS).map(|i| serde_json::json!(i)).collect();
330 data.insert("name".to_string(), serde_json::json!(large_array));
331
332 let result = validate_mutation_data(&data, &admin, false);
333 assert!(result.is_err());
334 let err = result.unwrap_err();
335 assert!(matches!(err, AdminError::ValidationError(_)));
336 assert!(err.to_string().contains("array too large"));
337 }
338
339 #[rstest]
340 fn test_validate_uses_list_display_as_fallback() {
341 let admin = ModelAdminConfig::builder()
343 .model_name("TestModel")
344 .list_display(vec!["id", "title"])
345 .build()
346 .unwrap();
347
348 let mut data = HashMap::new();
349 data.insert("title".to_string(), serde_json::json!("Test"));
350
351 assert!(validate_mutation_data(&data, &admin, false).is_ok());
352 }
353
354 #[rstest]
357 #[case::below_limit(99, true)]
358 #[case::at_limit(100, true)]
359 #[case::above_limit(101, false)]
360 fn test_mutation_field_count_boundary(#[case] field_count: usize, #[case] should_pass: bool) {
361 let field_names: Vec<String> = (0..field_count).map(|i| format!("f_{}", i)).collect();
364 let field_refs: Vec<&str> = field_names.iter().map(|s| s.as_str()).collect();
365 let admin = ModelAdminConfig::builder()
366 .model_name("TestModel")
367 .list_display(field_refs.clone())
368 .fields(field_refs)
369 .build()
370 .unwrap();
371
372 let mut data = HashMap::new();
373 for i in 0..field_count {
374 data.insert(format!("f_{}", i), serde_json::json!("v"));
375 }
376
377 let result = validate_mutation_data(&data, &admin, false);
379
380 assert_eq!(
382 result.is_ok(),
383 should_pass,
384 "field_count={}, expected pass={}, got {:?}",
385 field_count,
386 should_pass,
387 result
388 );
389 }
390
391 #[rstest]
394 #[case::within_limit(999_999, true)]
395 #[case::at_limit(1_000_000, true)]
396 #[case::above_limit(1_000_001, false)]
397 fn test_mutation_string_length_boundary(#[case] length: usize, #[case] should_pass: bool) {
398 let admin = create_test_admin();
400 let mut data = HashMap::new();
401 data.insert("name".to_string(), serde_json::json!("x".repeat(length)));
402
403 let result = validate_mutation_data(&data, &admin, false);
405
406 assert_eq!(
408 result.is_ok(),
409 should_pass,
410 "length={}, expected pass={}, got {:?}",
411 length,
412 should_pass,
413 result
414 );
415 }
416
417 #[rstest]
420 #[case::field_in_allowlist_not_readonly_create(true, false, false, true, true)]
421 #[case::field_in_allowlist_not_readonly_update(true, false, false, false, true)]
422 #[case::field_not_in_allowlist(false, false, false, true, false)]
423 #[case::field_is_readonly_on_create(true, true, false, true, false)]
424 #[case::field_is_readonly_on_update(true, true, false, false, false)]
425 #[case::pk_field_on_create(true, false, true, true, true)]
426 #[case::pk_field_on_update(true, false, true, false, false)]
427 fn test_mutation_validation_decision_table(
428 #[case] in_allowlist: bool,
429 #[case] is_readonly: bool,
430 #[case] is_pk: bool,
431 #[case] is_create: bool,
432 #[case] should_pass: bool,
433 ) {
434 let field_name = if is_pk { "id" } else { "name" };
436 let is_update = !is_create;
437
438 let mut fields_list = vec!["id"];
439 if in_allowlist && !is_pk {
440 fields_list.push("name");
441 }
442
443 let readonly = if is_readonly && !is_pk {
444 vec!["name"]
445 } else {
446 vec![]
447 };
448
449 let admin = ModelAdminConfig::builder()
450 .model_name("TestModel")
451 .list_display(fields_list.clone())
452 .fields(fields_list)
453 .readonly_fields(readonly)
454 .build()
455 .unwrap();
456
457 let mut data = HashMap::new();
458 data.insert(field_name.to_string(), serde_json::json!("test_value"));
459
460 let result = validate_mutation_data(&data, &admin, is_update);
462
463 assert_eq!(
465 result.is_ok(),
466 should_pass,
467 "in_allowlist={}, is_readonly={}, is_pk={}, is_create={}, expected pass={}, got {:?}",
468 in_allowlist,
469 is_readonly,
470 is_pk,
471 is_create,
472 should_pass,
473 result
474 );
475 }
476}