1use parse_rust_core::{
30 ClassLevelPermissions, ErrorCode, ErrorDetail, OpEntity, Operation, ParseError, ParseMap,
31 ParseValue, PfEntity, UserFieldsKey,
32};
33use parse_rust_storage::{ClassSchema, Comparison, Constraint, FieldType, Query, SortDirection};
34
35use crate::acl::AclScope;
36use crate::query_parse::ParsedWhere;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum WriteAction {
44 Create,
45 Update,
46}
47
48#[derive(Debug, Clone)]
50pub struct PermissionOptions {
51 pub protected_fields_owner_exempt: bool,
54 pub error_detail: ErrorDetail,
57 pub allow_client_class_creation: bool,
66}
67
68impl Default for PermissionOptions {
69 fn default() -> Self {
70 Self {
71 protected_fields_owner_exempt: true,
72 allow_client_class_creation: false,
73 error_detail: ErrorDetail::Withheld,
77 }
78 }
79}
80
81pub fn test_permissions(
87 clp: Option<&ClassLevelPermissions>,
88 acl_group: &[String],
89 operation: Operation,
90) -> bool {
91 let Some(clp) = clp else { return true };
92 let Some(perm) = clp.op(operation) else {
93 return true;
94 };
95 if perm.grants(&OpEntity::Public) {
96 return true;
97 }
98 acl_group.iter().any(|a| perm.grants(&OpEntity::parse(a)))
99}
100
101pub fn validate_permission(
109 clp: Option<&ClassLevelPermissions>,
110 class_name: &str,
111 acl_group: &[String],
112 operation: Operation,
113 action: Option<WriteAction>,
114 detail: ErrorDetail,
115) -> Result<(), ParseError> {
116 if test_permissions(clp, acl_group, operation) {
118 return Ok(());
119 }
120
121 let Some(clp) = clp else { return Ok(()) };
126 let Some(perm) = clp.op(operation) else {
127 return Ok(());
128 };
129
130 if perm.grants(&OpEntity::RequiresAuthentication) {
133 let anonymous = acl_group.is_empty() || acl_group == ["*"];
134 if anonymous {
135 return Err(ParseError::permission_denied(
136 ErrorCode::ObjectNotFound,
137 "Permission denied, user needs to be authenticated.",
138 detail,
139 ));
140 }
141 return Ok(());
144 }
145
146 if operation.user_fields_key() == UserFieldsKey::Write && operation == Operation::Create {
149 return Err(forbidden(class_name, operation, detail));
150 }
151
152 if !clp.user_fields(operation).is_empty() {
154 return Ok(());
155 }
156
157 if !perm.pointer_fields.is_empty()
160 && (operation != Operation::AddField || action == Some(WriteAction::Update))
161 {
162 return Ok(());
163 }
164
165 Err(forbidden(class_name, operation, detail))
167}
168
169fn forbidden(class_name: &str, operation: Operation, detail: ErrorDetail) -> ParseError {
170 ParseError::permission_denied(
171 ErrorCode::OperationForbidden,
172 format!(
173 "Permission denied for action {} on class {class_name}.",
174 operation.as_key()
175 ),
176 detail,
177 )
178}
179
180#[must_use]
184#[derive(Debug)]
185pub enum PointerPermOutcome {
186 Unconstrained,
188 Constrained(Query),
190 DenyAll,
193}
194
195pub fn apply_pointer_permissions(
199 schema: &ClassSchema,
200 clp: Option<&ClassLevelPermissions>,
201 operation: Operation,
202 acl_group: &[String],
203 query: &Query,
204) -> Result<PointerPermOutcome, ParseError> {
205 if test_permissions(clp, acl_group, operation) {
209 return Ok(PointerPermOutcome::Unconstrained);
210 }
211 let Some(clp) = clp else {
212 return Ok(PointerPermOutcome::Unconstrained);
213 };
214
215 let fields = clp.applicable_pointer_fields(operation);
218 if fields.is_empty() {
220 return Ok(PointerPermOutcome::Unconstrained);
221 }
222
223 let user_acl: Vec<&String> = acl_group
225 .iter()
226 .filter(|a| !a.starts_with("role:") && a.as_str() != "*")
227 .collect();
228
229 let [user_id] = user_acl.as_slice() else {
232 return Ok(PointerPermOutcome::DenyAll);
233 };
234
235 let user_pointer = ParseValue::Pointer {
236 class_name: "_User".to_string(),
237 object_id: (*user_id).clone(),
238 };
239
240 let mut alternatives = Vec::with_capacity(fields.len());
242 for field in &fields {
243 let constraint = match schema.field(field) {
244 Some(FieldType::Pointer { .. }) | Some(FieldType::Object) => {
245 Constraint::equal(field.clone(), user_pointer.clone())
246 }
247 Some(FieldType::Array) => Constraint {
248 field: field.clone(),
249 comparison: Comparison::All(vec![user_pointer.clone()]),
250 },
251 _ => {
261 let class_name = &schema.class_name;
262 return Err(ParseError::internal(format!(
263 "An unexpected condition occurred when resolving pointer permissions: \
264 {class_name} {field}"
265 )));
266 }
267 };
268 alternatives.push(Query::from_constraints(vec![constraint]));
269 }
270
271 let mut out = query.clone();
282 out.conjoin(Query::any_of(alternatives));
283 Ok(PointerPermOutcome::Constrained(out))
284}
285
286pub fn adds_field<'a>(
292 schema: &ClassSchema,
293 class_exists: bool,
294 keys: impl IntoIterator<Item = &'a str>,
295 is_delete: impl Fn(&str) -> bool,
296) -> bool {
297 if !class_exists {
298 return false;
299 }
300 keys.into_iter().any(|key| {
301 if is_delete(key) {
302 return false;
303 }
304 let root = key.split('.').next().unwrap_or(key);
307 schema.field(root).is_none()
308 })
309}
310
311#[derive(Debug, Clone, Default)]
318pub struct ProtectedFieldPlan {
319 pub strip: Vec<String>,
321 pub user_field_rules: Vec<(String, Vec<String>)>,
324}
325
326impl ProtectedFieldPlan {
327 pub fn is_empty(&self) -> bool {
328 self.strip.is_empty() && self.user_field_rules.is_empty()
329 }
330}
331
332pub fn plan_protected_fields(
339 class_name: &str,
340 clp: Option<&ClassLevelPermissions>,
341 scope: &AclScope,
342 pinned_object_id: Option<&str>,
343 options: &PermissionOptions,
344) -> Option<ProtectedFieldPlan> {
345 let clp = clp?;
347 if clp.protected_fields().is_empty() {
348 return None;
349 }
350
351 let acl_group = scope.acl_group();
354 if class_name == "_User"
355 && options.protected_fields_owner_exempt
356 && pinned_object_id.is_some_and(|id| acl_group.iter().any(|a| a == id))
357 {
358 return None;
359 }
360
361 let authenticated = scope.user_id().is_some();
363 let mut sets: Vec<&Vec<String>> = Vec::new();
364 let mut user_field_rules = Vec::new();
365
366 for (entity, fields) in clp.protected_fields() {
367 match entity {
368 PfEntity::UserField(name) => user_field_rules.push((name.clone(), fields.clone())),
370 PfEntity::Public => sets.push(fields),
371 PfEntity::Authenticated if authenticated => sets.push(fields),
372 PfEntity::Role(name) if authenticated && scope.has_role(name) => sets.push(fields),
373 _ => {}
374 }
375 }
376 if let Some(user_id) = scope.user_id() {
380 if let Some(fields) = clp
381 .protected_fields()
382 .get(&PfEntity::User(user_id.to_string()))
383 {
384 sets.push(fields);
385 }
386 }
387
388 Some(ProtectedFieldPlan {
389 strip: intersect_all(&sets),
390 user_field_rules,
391 })
392}
393
394fn intersect_all(sets: &[&Vec<String>]) -> Vec<String> {
399 let Some((first, rest)) = sets.split_first() else {
400 return Vec::new();
401 };
402 let mut out: Vec<String> = Vec::new();
403 for field in first.iter() {
404 if out.contains(field) {
405 continue;
406 }
407 if rest.iter().all(|set| set.contains(field)) {
408 out.push(field.clone());
409 }
410 }
411 out
412}
413
414pub fn deny_protected_fields(
422 plan: Option<&ProtectedFieldPlan>,
423 class_name: &str,
424 where_: &ParsedWhere,
425 order: &[(String, SortDirection)],
426 detail: ErrorDetail,
427) -> Result<(), ParseError> {
428 let Some(plan) = plan else { return Ok(()) };
429 if plan.strip.is_empty() {
430 return Ok(());
431 }
432
433 let denied = |key: &str| -> bool {
434 let root = key.split('.').next().unwrap_or(key);
437 plan.strip.iter().any(|f| f == key || f == root)
438 };
439
440 for key in where_.field_keys() {
441 if denied(&key) {
442 return Err(ParseError::permission_denied(
443 ErrorCode::OperationForbidden,
444 format!("This user is not allowed to query {key} on class {class_name}"),
445 detail,
446 ));
447 }
448 }
449 for (key, _) in order {
450 if denied(key) {
451 return Err(ParseError::permission_denied(
452 ErrorCode::OperationForbidden,
453 format!("This user is not allowed to sort by {key} on class {class_name}"),
454 detail,
455 ));
456 }
457 }
458 Ok(())
459}
460
461pub fn filter_sensitive_data(
481 row: &mut ParseMap,
482 class_name: &str,
483 scope: &AclScope,
484 plan: Option<&ProtectedFieldPlan>,
485 is_read: bool,
486 options: &PermissionOptions,
487) {
488 let is_user_class = class_name == "_User";
489 let acl_group = scope.acl_group();
490
491 let mut protected: Option<Vec<String>> = plan.map(|p| p.strip.clone());
493 if is_read {
494 if let Some(plan) = plan {
495 let matched: Vec<&Vec<String>> = plan
496 .user_field_rules
497 .iter()
498 .filter(|(field, _)| row_field_points_at(row.get(field), scope.user_id()))
499 .map(|(_, fields)| fields)
500 .collect();
501 if !matched.is_empty() {
502 let mut sets = matched;
506 if let Some(existing) = protected.as_ref() {
507 sets.push(existing);
508 }
509 protected = Some(intersect_all(&sets));
510 }
511 }
512 }
513
514 if is_user_class {
516 row.shift_remove("sessionToken");
517 }
518
519 let owner_exempt = options.protected_fields_owner_exempt
521 && is_user_class
522 && scope.user_id().is_some_and(
523 |uid| matches!(row.get("objectId"), Some(ParseValue::String(id)) if id == uid),
524 );
525 if !owner_exempt {
526 if let Some(fields) = protected {
527 for field in fields {
528 row.shift_remove(&field);
529 }
530 }
531 }
532
533 crate::guard::strip_internal_keys(row);
535
536 if !is_user_class || scope.is_master() {
538 return;
539 }
540 let own_row =
541 matches!(row.get("objectId"), Some(ParseValue::String(id)) if acl_group.contains(id));
542 if !own_row {
543 row.shift_remove("authData");
544 }
545}
546
547fn row_field_points_at(value: Option<&ParseValue>, user_id: Option<&str>) -> bool {
550 let Some(user_id) = user_id else { return false };
551 match value {
552 Some(ParseValue::Pointer { object_id, .. }) => object_id == user_id,
553 Some(ParseValue::Array(items)) => items
554 .iter()
555 .any(|v| matches!(v, ParseValue::Pointer { object_id, .. } if object_id == user_id)),
556 Some(ParseValue::Object(map)) => {
559 matches!(map.get("objectId"), Some(ParseValue::String(id)) if id == user_id)
560 }
561 _ => false,
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use parse_rust_core::ClassLevelPermissions;
569
570 fn clp(json: &str) -> ClassLevelPermissions {
571 let value = parse_rust_core::classify(
572 serde_json::from_str(json).expect("test literal must be valid JSON"),
573 )
574 .expect("classify");
575 match value {
576 ParseValue::Object(m) => ClassLevelPermissions::from_map(m),
577 _ => panic!("expected an object"),
578 }
579 }
580
581 fn user_scope() -> AclScope {
582 AclScope::user("u1", vec![]).expect("scope")
583 }
584
585 const WITHHELD: ErrorDetail = ErrorDetail::Withheld;
588 const DISCLOSED: ErrorDetail = ErrorDetail::Disclosed;
591
592 #[test]
593 fn no_clp_at_all_allows_every_operation() {
594 for op in Operation::ALL {
595 assert!(test_permissions(None, &["*".to_string()], op), "{op:?}");
596 assert!(
597 validate_permission(None, "Post", &["*".to_string()], op, None, DISCLOSED).is_ok()
598 );
599 }
600 }
601
602 #[test]
604 fn an_absent_operation_entry_is_unrestricted() {
605 let c = clp(r#"{"find":{}}"#);
606 assert!(
607 validate_permission(
608 Some(&c),
609 "Post",
610 &["*".into()],
611 Operation::Update,
612 None,
613 DISCLOSED
614 )
615 .is_ok(),
616 "update has no entry, so it is unrestricted"
617 );
618 let e = validate_permission(
619 Some(&c),
620 "Post",
621 &["*".into()],
622 Operation::Find,
623 None,
624 DISCLOSED,
625 )
626 .unwrap_err();
627 assert_eq!(e.code, ErrorCode::OperationForbidden);
628 assert_eq!(
629 e.message,
630 "Permission denied for action find on class Post."
631 );
632
633 let withheld = validate_permission(
635 Some(&c),
636 "Post",
637 &["*".into()],
638 Operation::Find,
639 None,
640 WITHHELD,
641 )
642 .unwrap_err();
643 assert_eq!(withheld.code, ErrorCode::OperationForbidden);
644 assert_eq!(withheld.message, "Permission denied");
645 }
646
647 #[test]
648 fn a_granted_role_passes_the_gate() {
649 let c = clp(r#"{"find":{"role:Admins":true}}"#);
650 let member = AclScope::user("u1", vec!["Admins".into()]).expect("scope");
651 assert!(validate_permission(
652 Some(&c),
653 "Post",
654 &member.acl_group(),
655 Operation::Find,
656 None,
657 WITHHELD
658 )
659 .is_ok());
660 let outsider = AclScope::user("u2", vec!["Others".into()]).expect("scope");
661 assert!(validate_permission(
662 Some(&c),
663 "Post",
664 &outsider.acl_group(),
665 Operation::Find,
666 None,
667 WITHHELD
668 )
669 .is_err());
670 }
671
672 #[test]
674 fn requires_authentication_denies_anonymously_with_object_not_found() {
675 let c = clp(r#"{"find":{"requiresAuthentication":true}}"#);
676 let e = validate_permission(
677 Some(&c),
678 "Post",
679 &AclScope::Anonymous.acl_group(),
680 Operation::Find,
681 None,
682 DISCLOSED,
683 )
684 .unwrap_err();
685 assert_eq!(e.code, ErrorCode::ObjectNotFound);
686 assert_eq!(
687 e.message,
688 "Permission denied, user needs to be authenticated."
689 );
690
691 let withheld = validate_permission(
694 Some(&c),
695 "Post",
696 &AclScope::Anonymous.acl_group(),
697 Operation::Find,
698 None,
699 WITHHELD,
700 )
701 .unwrap_err();
702 assert_eq!(withheld.code, ErrorCode::ObjectNotFound);
703 assert_eq!(withheld.message, "Permission denied");
704
705 assert_eq!(
707 validate_permission(Some(&c), "Post", &[], Operation::Find, None, WITHHELD)
708 .unwrap_err()
709 .code,
710 ErrorCode::ObjectNotFound
711 );
712
713 assert!(validate_permission(
714 Some(&c),
715 "Post",
716 &user_scope().acl_group(),
717 Operation::Find,
718 None,
719 WITHHELD
720 )
721 .is_ok());
722 }
723
724 #[test]
726 fn requires_authentication_with_pointer_fields_still_narrows_in_stage_two() {
727 let c = clp(r#"{"find":{"requiresAuthentication":true,"pointerFields":["owner"]}}"#);
728 let scope = user_scope();
729 assert!(validate_permission(
730 Some(&c),
731 "Post",
732 &scope.acl_group(),
733 Operation::Find,
734 None,
735 WITHHELD
736 )
737 .is_ok());
738
739 let schema = ClassSchema::new("Post").with_field(
740 "owner",
741 FieldType::Pointer {
742 target_class: "_User".into(),
743 },
744 );
745 match apply_pointer_permissions(
746 &schema,
747 Some(&c),
748 Operation::Find,
749 &scope.acl_group(),
750 &Query::new(),
751 )
752 .expect("no misconfiguration")
753 {
754 PointerPermOutcome::Constrained(q) => assert_eq!(q.clauses.len(), 1),
755 other => panic!("expected Constrained, got {other:?}"),
756 }
757 }
758
759 #[test]
760 fn write_user_fields_lock_down_create_only() {
761 let c = clp(r#"{"create":{},"update":{},"writeUserFields":["owner"]}"#);
762 let scope = user_scope();
763 let e = validate_permission(
764 Some(&c),
765 "Post",
766 &scope.acl_group(),
767 Operation::Create,
768 Some(WriteAction::Create),
769 DISCLOSED,
770 )
771 .unwrap_err();
772 assert_eq!(e.code, ErrorCode::OperationForbidden);
773 assert_eq!(
774 e.message,
775 "Permission denied for action create on class Post."
776 );
777 assert_eq!(
778 validate_permission(
779 Some(&c),
780 "Post",
781 &scope.acl_group(),
782 Operation::Create,
783 Some(WriteAction::Create),
784 WITHHELD,
785 )
786 .unwrap_err()
787 .message,
788 "Permission denied"
789 );
790 assert!(validate_permission(
791 Some(&c),
792 "Post",
793 &scope.acl_group(),
794 Operation::Update,
795 Some(WriteAction::Update),
796 WITHHELD
797 )
798 .is_ok());
799 }
800
801 #[test]
802 fn add_field_defers_only_on_update() {
803 let c = clp(r#"{"addField":{"pointerFields":["owner"]}}"#);
804 let scope = user_scope();
805 assert!(validate_permission(
806 Some(&c),
807 "Post",
808 &scope.acl_group(),
809 Operation::AddField,
810 Some(WriteAction::Update),
811 WITHHELD
812 )
813 .is_ok());
814 assert_eq!(
815 validate_permission(
816 Some(&c),
817 "Post",
818 &scope.acl_group(),
819 Operation::AddField,
820 Some(WriteAction::Create),
821 WITHHELD
822 )
823 .unwrap_err()
824 .code,
825 ErrorCode::OperationForbidden
826 );
827 }
828
829 #[test]
830 fn a_public_class_is_never_pointer_restricted() {
831 let c = clp(r#"{"find":{"*":true,"pointerFields":["owner"]}}"#);
832 let schema = ClassSchema::new("Post");
833 assert!(matches!(
834 apply_pointer_permissions(
835 &schema,
836 Some(&c),
837 Operation::Find,
838 &AclScope::Anonymous.acl_group(),
839 &Query::new()
840 )
841 .expect("ok"),
842 PointerPermOutcome::Unconstrained
843 ));
844 }
845
846 #[test]
847 fn an_anonymous_caller_is_denied_all_by_a_pointer_permission() {
848 let c = clp(r#"{"find":{"pointerFields":["owner"]}}"#);
849 let schema = ClassSchema::new("Post").with_field(
850 "owner",
851 FieldType::Pointer {
852 target_class: "_User".into(),
853 },
854 );
855 assert!(matches!(
856 apply_pointer_permissions(
857 &schema,
858 Some(&c),
859 Operation::Find,
860 &AclScope::Anonymous.acl_group(),
861 &Query::new()
862 )
863 .expect("ok"),
864 PointerPermOutcome::DenyAll
865 ));
866 }
867
868 #[test]
869 fn the_clause_shape_follows_the_schema_type() {
870 let c = clp(r#"{"find":{"pointerFields":["owner","editors","meta"]}}"#);
871 let schema = ClassSchema::new("Post")
872 .with_field(
873 "owner",
874 FieldType::Pointer {
875 target_class: "_User".into(),
876 },
877 )
878 .with_field("editors", FieldType::Array)
879 .with_field("meta", FieldType::Object);
880 let scope = user_scope();
881 let q = match apply_pointer_permissions(
882 &schema,
883 Some(&c),
884 Operation::Find,
885 &scope.acl_group(),
886 &Query::new(),
887 )
888 .expect("ok")
889 {
890 PointerPermOutcome::Constrained(q) => q,
891 other => panic!("expected Constrained, got {other:?}"),
892 };
893 match q.clauses.as_slice() {
895 [parse_rust_storage::Clause::Or(alts)] => {
896 assert_eq!(alts.len(), 3);
897 assert!(matches!(
898 alts[1].clauses[0],
899 parse_rust_storage::Clause::Field(Constraint {
900 comparison: Comparison::All(_),
901 ..
902 })
903 ));
904 }
905 other => panic!("expected a single Or clause, got {other:?}"),
906 }
907 }
908
909 #[test]
912 fn a_client_constraint_on_the_permission_field_survives_composition() {
913 let c = clp(r#"{"find":{"pointerFields":["owner"]}}"#);
914 let schema = ClassSchema::new("Post").with_field(
915 "owner",
916 FieldType::Pointer {
917 target_class: "_User".into(),
918 },
919 );
920 let scope = user_scope();
921 let client = Query::from_constraints(vec![Constraint::equal(
922 "owner",
923 ParseValue::Pointer {
924 class_name: "_User".into(),
925 object_id: "u1".into(),
926 },
927 )]);
928 let q = match apply_pointer_permissions(
929 &schema,
930 Some(&c),
931 Operation::Find,
932 &scope.acl_group(),
933 &client,
934 )
935 .expect("ok")
936 {
937 PointerPermOutcome::Constrained(q) => q,
938 other => panic!("expected Constrained, got {other:?}"),
939 };
940 assert!(matches!(
943 q.clauses.as_slice(),
944 [
945 parse_rust_storage::Clause::Field(f),
946 parse_rust_storage::Clause::And(nested)
947 ] if f.field == "owner" && nested.len() == 1
948 ));
949 }
950
951 #[test]
953 fn a_pointer_permission_on_an_unusable_field_type_is_a_500() {
954 let c = clp(r#"{"find":{"pointerFields":["title"]}}"#);
955 let schema = ClassSchema::new("Post").with_field("title", FieldType::String);
956 let e = apply_pointer_permissions(
957 &schema,
958 Some(&c),
959 Operation::Find,
960 &user_scope().acl_group(),
961 &Query::new(),
962 )
963 .unwrap_err();
964 assert_eq!(e.code, ErrorCode::InternalServerError);
965 assert_eq!(e.origin, parse_rust_core::ErrorOrigin::Internal);
968 assert!(e.message.contains("Post"));
969 assert!(e.message.contains("title"));
970
971 let c2 = clp(r#"{"find":{"pointerFields":["nope"]}}"#);
973 assert_eq!(
974 apply_pointer_permissions(
975 &ClassSchema::new("Post"),
976 Some(&c2),
977 Operation::Find,
978 &user_scope().acl_group(),
979 &Query::new()
980 )
981 .unwrap_err()
982 .code,
983 ErrorCode::InternalServerError
984 );
985 }
986
987 #[test]
988 fn protected_fields_intersect_rather_than_union() {
989 let c = clp(
990 r#"{"protectedFields":{"*":["email","phone","ssn"],"authenticated":["email","phone"],"role:A":["email"]}}"#,
991 );
992
993 let anon = plan_protected_fields(
995 "Post",
996 Some(&c),
997 &AclScope::Anonymous,
998 None,
999 &PermissionOptions::default(),
1000 )
1001 .expect("plan");
1002 assert_eq!(anon.strip, vec!["email", "phone", "ssn"]);
1003
1004 let user = AclScope::user("u1", vec![]).expect("scope");
1006 let plan =
1007 plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
1008 .expect("plan");
1009 assert_eq!(plan.strip, vec!["email", "phone"]);
1010
1011 let admin = AclScope::user("u1", vec!["A".into()]).expect("scope");
1013 let plan = plan_protected_fields(
1014 "Post",
1015 Some(&c),
1016 &admin,
1017 None,
1018 &PermissionOptions::default(),
1019 )
1020 .expect("plan");
1021 assert_eq!(plan.strip, vec!["email"]);
1022 }
1023
1024 #[test]
1027 fn an_inapplicable_role_contributes_nothing() {
1028 let c = clp(r#"{"protectedFields":{"*":["email"],"role:A":["phone"]}}"#);
1029 let outsider = AclScope::user("u1", vec!["B".into()]).expect("scope");
1030 let plan = plan_protected_fields(
1031 "Post",
1032 Some(&c),
1033 &outsider,
1034 None,
1035 &PermissionOptions::default(),
1036 )
1037 .expect("plan");
1038 assert_eq!(
1039 plan.strip,
1040 vec!["email"],
1041 "a role the caller does not hold must not join the intersection"
1042 );
1043 }
1044
1045 #[test]
1046 fn the_user_owner_exemption_defaults_to_exempt_and_honors_the_option() {
1047 let c = clp(r#"{"protectedFields":{"*":["email"]}}"#);
1048 let user = AclScope::user("u1", vec![]).expect("scope");
1049 assert!(
1050 plan_protected_fields(
1051 "_User",
1052 Some(&c),
1053 &user,
1054 Some("u1"),
1055 &PermissionOptions::default()
1056 )
1057 .is_none(),
1058 "an unset option means exempt"
1059 );
1060 let strict = PermissionOptions {
1061 protected_fields_owner_exempt: false,
1062 ..PermissionOptions::default()
1063 };
1064 assert!(plan_protected_fields("_User", Some(&c), &user, Some("u1"), &strict).is_some());
1065 assert!(plan_protected_fields(
1067 "_User",
1068 Some(&c),
1069 &user,
1070 Some("u2"),
1071 &PermissionOptions::default()
1072 )
1073 .is_some());
1074 }
1075
1076 #[test]
1077 fn a_user_field_rule_matching_the_row_reduces_what_is_stripped() {
1078 let c = clp(r#"{"protectedFields":{"*":["email","phone"],"userField:owner":["phone"]}}"#);
1079 let user = AclScope::user("u1", vec![]).expect("scope");
1080 let plan =
1081 plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
1082 .expect("plan");
1083 assert_eq!(plan.strip, vec!["email", "phone"]);
1084 assert_eq!(plan.user_field_rules.len(), 1);
1085
1086 let mut row = ParseMap::new();
1087 row.insert("objectId".into(), ParseValue::String("p1".into()));
1088 row.insert(
1089 "owner".into(),
1090 ParseValue::Pointer {
1091 class_name: "_User".into(),
1092 object_id: "u1".into(),
1093 },
1094 );
1095 row.insert("email".into(), ParseValue::String("a@b.c".into()));
1096 row.insert("phone".into(), ParseValue::String("555".into()));
1097 filter_sensitive_data(
1098 &mut row,
1099 "Post",
1100 &user,
1101 Some(&plan),
1102 true,
1103 &PermissionOptions::default(),
1104 );
1105 assert!(row.get("email").is_some());
1108 assert!(row.get("phone").is_none());
1109
1110 let mut other = ParseMap::new();
1112 other.insert(
1113 "owner".into(),
1114 ParseValue::Pointer {
1115 class_name: "_User".into(),
1116 object_id: "u2".into(),
1117 },
1118 );
1119 other.insert("email".into(), ParseValue::String("a@b.c".into()));
1120 other.insert("phone".into(), ParseValue::String("555".into()));
1121 filter_sensitive_data(
1122 &mut other,
1123 "Post",
1124 &user,
1125 Some(&plan),
1126 true,
1127 &PermissionOptions::default(),
1128 );
1129 assert!(other.get("phone").is_none());
1130 assert!(
1131 other.get("email").is_none(),
1132 "no rule matched, so the pre-query list stands and both are stripped"
1133 );
1134 }
1135
1136 #[test]
1149 fn a_user_field_rule_that_cannot_be_evaluated_protects_more_not_less() {
1150 let c = clp(r#"{"protectedFields":{"*":["email","phone"],"userField:owner":["phone"]}}"#);
1151 let user = AclScope::user("u1", vec![]).expect("scope");
1152 let plan =
1153 plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
1154 .expect("plan");
1155
1156 let mut projected = ParseMap::new();
1158 projected.insert("email".into(), ParseValue::String("a@b.c".into()));
1159 projected.insert("phone".into(), ParseValue::String("555".into()));
1160 filter_sensitive_data(
1161 &mut projected,
1162 "Post",
1163 &user,
1164 Some(&plan),
1165 true,
1166 &PermissionOptions::default(),
1167 );
1168 assert!(
1169 projected.get("phone").is_none(),
1170 "the rule cannot match without `owner`, so `phone` stays protected"
1171 );
1172 assert!(
1173 projected.get("email").is_none(),
1174 "and so does `email`: the unreduced list is the one that applies"
1175 );
1176
1177 let mut full = ParseMap::new();
1180 full.insert(
1181 "owner".into(),
1182 ParseValue::Pointer {
1183 class_name: "_User".into(),
1184 object_id: "u1".into(),
1185 },
1186 );
1187 full.insert("email".into(), ParseValue::String("a@b.c".into()));
1188 full.insert("phone".into(), ParseValue::String("555".into()));
1189 filter_sensitive_data(
1190 &mut full,
1191 "Post",
1192 &user,
1193 Some(&plan),
1194 true,
1195 &PermissionOptions::default(),
1196 );
1197 assert!(full.get("email").is_some());
1198 }
1199
1200 #[test]
1207 fn a_principal_shaped_role_name_is_not_unwrapped() {
1208 let scope = AclScope::user("u1", vec!["role:Admin".to_string()]).expect("scope");
1209 let group = scope.acl_group();
1210 assert!(
1211 group.iter().any(|g| g == "role:role:Admin"),
1212 "the name is prefixed once, not collapsed: {group:?}"
1213 );
1214 assert!(
1215 !group.iter().any(|g| g == "role:Admin"),
1216 "holding a role named `role:Admin` must not grant `Admin`: {group:?}"
1217 );
1218
1219 let doubled = clp(r#"{"find":{"role:role:Admin":true}}"#);
1221 assert!(test_permissions(
1222 Some(&doubled),
1223 &group,
1224 parse_rust_core::Operation::Find
1225 ));
1226 let single = clp(r#"{"find":{"role:Admin":true}}"#);
1227 assert!(!test_permissions(
1228 Some(&single),
1229 &group,
1230 parse_rust_core::Operation::Find
1231 ));
1232 }
1233
1234 #[test]
1235 fn auth_data_survives_only_for_master_and_the_row_owner() {
1236 let mut row = ParseMap::new();
1237 row.insert("objectId".into(), ParseValue::String("u1".into()));
1238 row.insert("authData".into(), ParseValue::Object(ParseMap::new()));
1239 let owner = AclScope::user("u1", vec![]).expect("scope");
1240 filter_sensitive_data(
1241 &mut row,
1242 "_User",
1243 &owner,
1244 None,
1245 true,
1246 &PermissionOptions::default(),
1247 );
1248 assert!(row.get("authData").is_some());
1249
1250 let mut row = ParseMap::new();
1251 row.insert("objectId".into(), ParseValue::String("u1".into()));
1252 row.insert("authData".into(), ParseValue::Object(ParseMap::new()));
1253 let stranger = AclScope::user("u2", vec![]).expect("scope");
1254 filter_sensitive_data(
1255 &mut row,
1256 "_User",
1257 &stranger,
1258 None,
1259 true,
1260 &PermissionOptions::default(),
1261 );
1262 assert!(row.get("authData").is_none());
1263 }
1264
1265 #[test]
1266 fn the_password_hash_never_reaches_a_response_under_any_name() {
1267 let mut row = ParseMap::new();
1268 row.insert("objectId".into(), ParseValue::String("u1".into()));
1269 row.insert("_hashed_password".into(), ParseValue::String("hash".into()));
1270 row.insert("sessionToken".into(), ParseValue::String("r:t".into()));
1271 filter_sensitive_data(
1272 &mut row,
1273 "_User",
1274 &AclScope::user("u1", vec![]).expect("scope"),
1275 None,
1276 true,
1277 &PermissionOptions::default(),
1278 );
1279 assert!(row.get("_hashed_password").is_none());
1280 assert!(
1281 row.get("password").is_none(),
1282 "the hash is never rehydrated under a user-facing name"
1283 );
1284 assert!(row.get("sessionToken").is_none());
1285 }
1286
1287 #[test]
1288 fn adds_field_ignores_deletes_and_dotted_roots() {
1289 let schema = ClassSchema::new("Post").with_field("meta", FieldType::Object);
1290 assert!(!adds_field(&schema, true, ["meta.x"], |_| false));
1291 assert!(adds_field(&schema, true, ["fresh"], |_| false));
1292 assert!(!adds_field(&schema, true, ["fresh"], |k| k == "fresh"));
1293 assert!(
1294 !adds_field(&schema, false, ["fresh"], |_| false),
1295 "a write that creates the class never runs the addField gate"
1296 );
1297 }
1298}