1use parse_rust_core::{
25 js_number, ClassLevelPermissions, ErrorCode, ParseError, ParseMap, ParseValue,
26};
27use parse_rust_storage::ClassSchema;
28
29use crate::infer::DEFAULT_COLUMNS;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum ObjectIdForm {
37 Generated,
39 Custom,
41}
42
43impl ObjectIdForm {
44 fn accepts(self, key: &str) -> bool {
45 match self {
46 ObjectIdForm::Generated => {
47 !key.is_empty() && key.chars().all(|c| c.is_ascii_alphanumeric())
48 }
49 ObjectIdForm::Custom => !key.is_empty(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Unenforceable {
63 Refuse,
65 Accept,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct ClpValidation {
76 pub object_id: ObjectIdForm,
77 pub unenforceable: Unenforceable,
78}
79
80pub const VALID_KEYS: [&str; 11] = [
82 "ACL",
83 "find",
84 "count",
85 "get",
86 "create",
87 "update",
88 "delete",
89 "addField",
90 "readUserFields",
91 "writeUserFields",
92 "protectedFields",
93];
94
95pub fn validate_clp(
106 raw: ParseMap,
107 schema: &ClassSchema,
108 opts: ClpValidation,
109) -> Result<ClassLevelPermissions, ParseError> {
110 for (operation_key, operation) in &raw {
111 if !VALID_KEYS.contains(&operation_key.as_str()) {
112 return Err(ParseError::invalid_json(format!(
114 "{operation_key} is not a valid operation for class level permissions"
115 )));
116 }
117
118 validate_clp_json(operation, operation_key)?;
119
120 if operation_key == "readUserFields" || operation_key == "writeUserFields" {
121 if opts.unenforceable == Unenforceable::Refuse {
122 return Err(unenforceable(operation_key));
123 }
124 if let ParseValue::Array(items) = operation {
126 for item in items {
127 validate_pointer_permission(item, schema, operation_key)?;
128 }
129 }
130 continue;
131 }
132
133 if operation_key == "protectedFields" {
134 let entries = js_own_entries(operation);
135 for (entity, protected) in &entries {
136 let (entity, protected) = (entity.as_str(), protected);
137 validate_protected_fields_key(entity, opts.object_id)?;
138 if opts.unenforceable == Unenforceable::Refuse && entity.starts_with("userField:") {
139 return Err(unenforceable(entity));
140 }
141
142 let ParseValue::Array(fields) = protected else {
143 return Err(ParseError::invalid_json(format!(
144 "'{}' is not a valid value for protectedFields[{entity}] - expected an \
145 array.",
146 js_string(protected)
147 )));
148 };
149
150 for field in fields {
151 let name = js_string(field);
152 if DEFAULT_COLUMNS.iter().any(|(n, _)| *n == name) {
155 return Err(ParseError::invalid_json(format!(
156 "Default field '{name}' can not be protected"
157 )));
158 }
159 if !schema.fields.contains_key(&name) {
160 return Err(ParseError::invalid_json(format!(
161 "Field '{name}' in protectedFields:{entity} does not exist"
162 )));
163 }
164 }
165 }
166 continue;
167 }
168
169 let entries = js_own_entries(operation);
170 for (entity, permit) in &entries {
171 let (entity, permit) = (entity.as_str(), permit);
172 validate_permission_key(entity, opts.object_id)?;
173
174 if entity == "pointerFields" {
175 let ParseValue::Array(pointer_fields) = permit else {
176 return Err(ParseError::invalid_json(format!(
177 "'{}' is not a valid value for {operation_key}[{entity}] - expected an \
178 array.",
179 js_string(permit)
180 )));
181 };
182 for pointer_field in pointer_fields {
183 validate_pointer_permission(pointer_field, schema, "[object Object]")?;
189 }
190 continue;
191 }
192
193 if operation_key == "ACL" {
194 validate_clp_acl_entry(permit)?;
195 } else if !matches!(permit, ParseValue::Bool(true)) {
196 return Err(ParseError::invalid_json(format!(
199 "'{}' is not a valid value for class level permissions acl \
200 {operation_key}:{entity}",
201 js_string(permit)
202 )));
203 }
204 }
205 }
206
207 Ok(ClassLevelPermissions::from_map(raw))
208}
209
210fn unenforceable(key: &str) -> ParseError {
217 ParseError::new(
218 ErrorCode::CommandUnavailable,
219 format!(
220 "{key} is not supported yet. parse-rust validates it and cannot enforce it, so the \
221 class level permissions are refused rather than stored unenforced."
222 ),
223 )
224}
225
226fn validate_clp_json(operation: &ParseValue, operation_key: &str) -> Result<(), ParseError> {
228 if operation_key == "readUserFields" || operation_key == "writeUserFields" {
229 if !matches!(operation, ParseValue::Array(_)) {
230 return Err(ParseError::invalid_json(format!(
231 "'{}' is not a valid value for class level permissions {operation_key} - must be \
232 an array",
233 js_string(operation)
234 )));
235 }
236 return Ok(());
237 }
238 if is_js_object(operation) {
242 return Ok(());
243 }
244 Err(ParseError::invalid_json(format!(
245 "'{}' is not a valid value for class level permissions {operation_key} - must be an object",
246 js_string(operation)
247 )))
248}
249
250fn validate_permission_key(key: &str, object_id: ObjectIdForm) -> Result<(), ParseError> {
256 let matches_some = key == "pointerFields"
257 || key == "*"
258 || key == "requiresAuthentication"
259 || key.starts_with("role:");
260 if matches_some || object_id.accepts(key) {
261 return Ok(());
262 }
263 Err(invalid_clp_key(key))
264}
265
266fn validate_protected_fields_key(key: &str, object_id: ObjectIdForm) -> Result<(), ParseError> {
273 let matches_some = key.starts_with("userField:")
274 || key == "*"
275 || key == "authenticated"
276 || key.starts_with("role:");
277 if matches_some || object_id.accepts(key) {
278 return Ok(());
279 }
280 Err(invalid_clp_key(key))
281}
282
283fn invalid_clp_key(key: &str) -> ParseError {
285 ParseError::invalid_json(format!(
286 "'{key}' is not a valid key for class level permissions"
287 ))
288}
289
290fn validate_pointer_permission(
296 field: &ParseValue,
297 schema: &ClassSchema,
298 operation: &str,
299) -> Result<(), ParseError> {
300 let name = js_string(field);
301 let ok = match schema.fields.get(&name) {
302 Some(ty) => {
303 ty.target_class() == Some("_User") && ty.is_pointer()
304 || matches!(ty, parse_rust_storage::FieldType::Array)
305 }
306 None => false,
307 };
308 if ok {
309 return Ok(());
310 }
311 Err(ParseError::invalid_json(format!(
312 "'{name}' is not a valid column for class level pointer permissions {operation}"
313 )))
314}
315
316fn validate_clp_acl_entry(permit: &ParseValue) -> Result<(), ParseError> {
322 let ParseValue::Object(entry) = permit else {
325 return Err(ParseError::invalid_json(format!(
326 "'{}' is not a valid value for class level permissions acl",
327 js_string(permit)
328 )));
329 };
330
331 let invalid_keys: Vec<&str> = entry
332 .keys()
333 .filter(|k| k.as_str() != "read" && k.as_str() != "write")
334 .map(String::as_str)
335 .collect();
336 if !invalid_keys.is_empty() {
337 return Err(ParseError::invalid_json(format!(
338 "'{}' is not a valid key for class level permissions acl",
339 invalid_keys.join(",")
340 )));
341 }
342
343 let invalid_values: Vec<String> = entry
344 .values()
345 .filter(|v| !matches!(v, ParseValue::Bool(_)))
346 .map(js_string)
347 .collect();
348 if !invalid_values.is_empty() {
349 return Err(ParseError::invalid_json(format!(
350 "'{}' is not a valid value for class level permissions acl",
351 invalid_values.join(",")
352 )));
353 }
354 Ok(())
355}
356
357fn js_own_entries(value: &ParseValue) -> Vec<(String, ParseValue)> {
379 let tagged = |pairs: Vec<(&str, ParseValue)>| {
380 pairs
381 .into_iter()
382 .map(|(k, v)| (k.to_string(), v))
383 .collect::<Vec<_>>()
384 };
385 let s = |v: &str| ParseValue::String(v.to_string());
386 match value {
387 ParseValue::Object(map) => map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
388 ParseValue::Array(items) => items
389 .iter()
390 .enumerate()
391 .map(|(i, v)| (i.to_string(), v.clone()))
392 .collect(),
393 ParseValue::String(text) => text
395 .chars()
396 .enumerate()
397 .map(|(i, c)| (i.to_string(), ParseValue::String(c.to_string())))
398 .collect(),
399 ParseValue::Date(d) => tagged(vec![("__type", s("Date")), ("iso", s(&d.to_iso()))]),
400 ParseValue::Pointer {
401 class_name,
402 object_id,
403 } => tagged(vec![
404 ("__type", s("Pointer")),
405 ("className", s(class_name)),
406 ("objectId", s(object_id)),
407 ]),
408 ParseValue::GeoPoint {
409 latitude,
410 longitude,
411 } => tagged(vec![
412 ("__type", s("GeoPoint")),
413 ("latitude", ParseValue::Number(*latitude)),
414 ("longitude", ParseValue::Number(*longitude)),
415 ]),
416 ParseValue::Bytes(_) => tagged(vec![("__type", s("Bytes")), ("base64", s(""))]),
417 ParseValue::File { name, .. } => tagged(vec![("__type", s("File")), ("name", s(name))]),
418 ParseValue::Polygon(_) => tagged(vec![
419 ("__type", s("Polygon")),
420 ("coordinates", ParseValue::Array(Vec::new())),
421 ]),
422 ParseValue::Relation { class_name } => tagged(vec![
423 ("__type", s("Relation")),
424 ("className", s(class_name)),
425 ]),
426 ParseValue::Null | ParseValue::Bool(_) | ParseValue::Number(_) => Vec::new(),
427 }
428}
429
430fn is_js_object(value: &ParseValue) -> bool {
435 !matches!(
436 value,
437 ParseValue::Null | ParseValue::Bool(_) | ParseValue::Number(_) | ParseValue::String(_)
438 )
439}
440
441fn js_string(value: &ParseValue) -> String {
451 match value {
452 ParseValue::Null => "null".to_string(),
453 ParseValue::Bool(b) => b.to_string(),
454 ParseValue::Number(n) => js_number::to_ecma_string(*n),
455 ParseValue::String(s) => s.clone(),
456 ParseValue::Array(items) => items
457 .iter()
458 .map(|item| match item {
459 ParseValue::Null => String::new(),
461 other => js_string(other),
462 })
463 .collect::<Vec<_>>()
464 .join(","),
465 _ => "[object Object]".to_string(),
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use parse_rust_core::{classify, OpEntity, Operation, PfEntity};
473 use parse_rust_storage::FieldType;
474
475 fn opts() -> ClpValidation {
476 ClpValidation {
477 object_id: ObjectIdForm::Generated,
478 unenforceable: Unenforceable::Accept,
479 }
480 }
481
482 fn map(json: &str) -> ParseMap {
483 match classify(serde_json::from_str(json).expect("test literal must be valid JSON"))
484 .expect("classify")
485 {
486 ParseValue::Object(m) => m,
487 other => panic!("expected an object, got {other:?}"),
488 }
489 }
490
491 fn schema() -> ClassSchema {
492 crate::controller::default_schema("Post")
493 .with_field("title", FieldType::String)
494 .with_field(
495 "owner",
496 FieldType::Pointer {
497 target_class: "_User".into(),
498 },
499 )
500 .with_field(
501 "author",
502 FieldType::Pointer {
503 target_class: "Writer".into(),
504 },
505 )
506 .with_field("editors", FieldType::Array)
507 }
508
509 fn err(json: &str) -> ParseError {
510 validate_clp(map(json), &schema(), opts()).expect_err("should be rejected")
511 }
512
513 fn ok(json: &str) -> ClassLevelPermissions {
514 validate_clp(map(json), &schema(), opts()).expect("should be accepted")
515 }
516
517 #[test]
518 fn an_unknown_top_level_key_is_refused_without_quotes() {
519 let e = err(r#"{"nope":{"*":true}}"#);
520 assert_eq!(
521 e.message,
522 "nope is not a valid operation for class level permissions"
523 );
524 assert_eq!(e.code, ErrorCode::InvalidJson);
525 }
526
527 #[test]
528 fn every_valid_top_level_key_is_accepted() {
529 assert_eq!(VALID_KEYS.len(), 11);
531 ok(r#"{
532 "ACL":{"*":{"read":true,"write":true}},
533 "find":{"*":true},"count":{"*":true},"get":{"*":true},
534 "create":{"*":true},"update":{"*":true},"delete":{"*":true},
535 "addField":{"*":true},
536 "readUserFields":["owner"],"writeUserFields":["owner"],
537 "protectedFields":{"*":["title"]}
538 }"#);
539 }
540
541 #[test]
542 fn only_literal_true_grants_an_operation() {
543 for (json, rendered) in [
544 (r#"{"find":{"*":false}}"#, "false"),
545 (r#"{"find":{"*":0}}"#, "0"),
546 (r#"{"find":{"*":"true"}}"#, "true"),
547 (r#"{"find":{"*":null}}"#, "null"),
548 (r#"{"find":{"*":1}}"#, "1"),
549 ] {
550 let e = err(json);
551 assert_eq!(
552 e.message,
553 format!("'{rendered}' is not a valid value for class level permissions acl find:*"),
554 "{json}"
555 );
556 }
557 }
558
559 #[test]
561 fn numbers_render_through_the_ecmascript_algorithm() {
562 assert_eq!(js_string(&ParseValue::Number(1.0)), "1");
563 assert_eq!(js_string(&ParseValue::Number(1.5)), "1.5");
564 assert_eq!(js_string(&ParseValue::Number(-0.0)), "0");
565 }
566
567 #[test]
568 fn a_non_object_operation_is_refused_before_its_entities() {
569 let e = err(r#"{"find":true}"#);
570 assert_eq!(
571 e.message,
572 "'true' is not a valid value for class level permissions find - must be an object"
573 );
574 let e = err(r#"{"readUserFields":"owner"}"#);
576 assert_eq!(
577 e.message,
578 "'owner' is not a valid value for class level permissions readUserFields - must be an \
579 array"
580 );
581 }
582
583 #[test]
584 fn the_two_entity_grammars_reject_each_others_spellings() {
585 let e = err(r#"{"find":{"has-dash":true}}"#);
589 assert_eq!(
590 e.message,
591 "'has-dash' is not a valid key for class level permissions"
592 );
593 let e = err(r#"{"protectedFields":{"has-dash":["title"]}}"#);
594 assert_eq!(
595 e.message,
596 "'has-dash' is not a valid key for class level permissions"
597 );
598 }
599
600 #[test]
601 fn a_custom_object_id_configuration_widens_the_entity_grammar() {
602 let custom = ClpValidation {
603 object_id: ObjectIdForm::Custom,
604 unenforceable: Unenforceable::Accept,
605 };
606 assert!(validate_clp(map(r#"{"find":{"has-dash":true}}"#), &schema(), opts()).is_err());
609 assert!(validate_clp(map(r#"{"find":{"has-dash":true}}"#), &schema(), custom).is_ok());
610 assert!(validate_clp(map(r#"{"find":{"":true}}"#), &schema(), custom).is_err());
612 }
613
614 #[test]
616 fn an_empty_role_name_is_accepted() {
617 let c = ok(r#"{"find":{"role:":true}}"#);
618 let perm = c.op(Operation::Find).expect("find is present");
619 assert_eq!(perm.entities, vec![OpEntity::Role(String::new())]);
620 }
621
622 #[test]
623 fn pointer_fields_must_be_an_array_of_user_pointers_or_arrays() {
624 ok(r#"{"find":{"pointerFields":["owner"]}}"#);
625 ok(r#"{"find":{"pointerFields":["editors"]}}"#);
626
627 let e = err(r#"{"find":{"pointerFields":["author"]}}"#);
629 assert_eq!(
630 e.message,
631 "'author' is not a valid column for class level pointer permissions [object Object]"
632 );
633 assert!(err(r#"{"find":{"pointerFields":["title"]}}"#)
635 .message
636 .starts_with("'title' is not a valid column"));
637 assert!(err(r#"{"find":{"pointerFields":["ghost"]}}"#)
638 .message
639 .starts_with("'ghost' is not a valid column"));
640 }
641
642 #[test]
646 fn the_pointer_permission_message_differs_between_the_two_call_sites() {
647 assert_eq!(
648 err(r#"{"readUserFields":["title"]}"#).message,
649 "'title' is not a valid column for class level pointer permissions readUserFields"
650 );
651 assert_eq!(
652 err(r#"{"writeUserFields":["title"]}"#).message,
653 "'title' is not a valid column for class level pointer permissions writeUserFields"
654 );
655 assert_eq!(
656 err(r#"{"find":{"pointerFields":["title"]}}"#).message,
657 "'title' is not a valid column for class level pointer permissions [object Object]"
658 );
659 }
660
661 #[test]
662 fn a_non_array_pointer_fields_names_the_operation_and_the_entity() {
663 let e = err(r#"{"update":{"pointerFields":"owner"}}"#);
664 assert_eq!(
665 e.message,
666 "'owner' is not a valid value for update[pointerFields] - expected an array."
667 );
668 }
669
670 #[test]
671 fn protected_fields_must_be_arrays_of_existing_non_default_fields() {
672 let c = ok(r#"{"protectedFields":{"*":["title"],"role:A":["title","owner"]}}"#);
673 assert_eq!(
674 c.protected_fields().get(&PfEntity::Public),
675 Some(&vec!["title".to_string()])
676 );
677
678 let e = err(r#"{"protectedFields":{"*":"title"}}"#);
679 assert_eq!(
680 e.message,
681 "'title' is not a valid value for protectedFields[*] - expected an array."
682 );
683
684 let e = err(r#"{"protectedFields":{"role:A":["ghost"]}}"#);
685 assert_eq!(
686 e.message,
687 "Field 'ghost' in protectedFields:role:A does not exist"
688 );
689 }
690
691 #[test]
694 fn no_default_column_can_be_protected() {
695 for column in ["objectId", "createdAt", "updatedAt", "ACL"] {
696 let e = err(&format!(r#"{{"protectedFields":{{"*":["{column}"]}}}}"#));
697 assert_eq!(
698 e.message,
699 format!("Default field '{column}' can not be protected")
700 );
701 }
702 }
703
704 #[test]
705 fn the_clp_acl_key_takes_read_and_write_booleans() {
706 ok(r#"{"ACL":{"*":{"read":true,"write":false}}}"#);
707
708 let e = err(r#"{"ACL":{"*":true}}"#);
709 assert_eq!(
710 e.message,
711 "'true' is not a valid value for class level permissions acl"
712 );
713
714 let e = err(r#"{"ACL":{"*":{"read":true,"delete":true,"update":true}}}"#);
715 assert_eq!(
716 e.message,
717 "'delete,update' is not a valid key for class level permissions acl"
718 );
719
720 let e = err(r#"{"ACL":{"*":{"read":1,"write":"yes"}}}"#);
721 assert_eq!(
722 e.message,
723 "'1,yes' is not a valid value for class level permissions acl"
724 );
725 }
726
727 #[test]
729 fn unenforceable_features_are_refused_not_ignored() {
730 let refuse = ClpValidation {
731 object_id: ObjectIdForm::Generated,
732 unenforceable: Unenforceable::Refuse,
733 };
734 for json in [
735 r#"{"readUserFields":["owner"]}"#,
736 r#"{"writeUserFields":["owner"]}"#,
737 r#"{"protectedFields":{"userField:owner":["title"]}}"#,
738 ] {
739 let e = validate_clp(map(json), &schema(), refuse).expect_err("must be refused");
740 assert_eq!(e.code, ErrorCode::CommandUnavailable, "{json}");
741 }
742 for json in [
745 r#"{"readUserFields":["owner"]}"#,
746 r#"{"writeUserFields":["owner"]}"#,
747 r#"{"protectedFields":{"userField:owner":["title"]}}"#,
748 ] {
749 assert!(validate_clp(map(json), &schema(), opts()).is_ok(), "{json}");
750 }
751 }
752
753 #[test]
755 fn refusing_user_field_entries_does_not_refuse_ordinary_ones() {
756 let refuse = ClpValidation {
757 object_id: ObjectIdForm::Generated,
758 unenforceable: Unenforceable::Refuse,
759 };
760 assert!(validate_clp(
761 map(r#"{"protectedFields":{"*":["title"],"role:A":["title"],"authenticated":["title"]}}"#),
762 &schema(),
763 refuse
764 )
765 .is_ok());
766 }
767
768 #[test]
771 fn the_raw_block_survives_validation_unchanged() {
772 let c = ok(r#"{"find":{"*":true},"ACL":{"*":{"read":true}}}"#);
773 assert!(c.raw().contains_key("ACL"));
774 assert!(c.raw().contains_key("find"));
775 assert_eq!(c.raw().len(), 2);
776 }
777
778 #[test]
780 fn an_empty_block_validates() {
781 let c = ok("{}");
782 assert!(c.op(Operation::Find).is_none());
783 assert!(c.raw().is_empty());
784 }
785
786 #[test]
789 fn an_array_operation_value_is_refused() {
790 let err = validate_clp(map(r#"{"find": ["*"]}"#), &schema(), opts())
791 .expect_err("an array is not a permission object");
792 assert_eq!(err.code, parse_rust_core::ErrorCode::InvalidJson);
793 }
794
795 #[test]
798 fn a_tagged_operation_value_is_refused_rather_than_silently_locking_the_class() {
799 let err = validate_clp(
800 map(r#"{"find": {"__type": "Date", "iso": "2026-01-01T00:00:00.000Z"}}"#),
801 &schema(),
802 opts(),
803 )
804 .expect_err("a Date is not a permission object");
805 assert_eq!(err.code, parse_rust_core::ErrorCode::InvalidJson);
806 }
807
808 #[test]
810 fn an_array_protected_fields_value_is_refused() {
811 let err = validate_clp(map(r#"{"protectedFields": ["title"]}"#), &schema(), opts())
812 .expect_err("an array is not a protectedFields object");
813 assert_eq!(err.code, parse_rust_core::ErrorCode::InvalidJson);
814 }
815
816 #[test]
819 fn a_primitive_operation_value_yields_no_entries() {
820 assert!(js_own_entries(&ParseValue::Number(1.0)).is_empty());
821 assert!(js_own_entries(&ParseValue::Bool(true)).is_empty());
822 assert!(js_own_entries(&ParseValue::Null).is_empty());
823 }
824}