1use std::future::Future;
21use std::pin::Pin;
22
23use indexmap::IndexMap;
24use parse_rust_core::{
25 new_object_id, ErrorCode, FieldWrite, Op, Operation, ParseDate, ParseError, ParseMap,
26 ParseValue,
27};
28use parse_rust_schema::{
29 apply, default_schema, field_name_is_valid, infer::schema_mismatch, infer_op_type, infer_type,
30 validate_required_columns, validate_write_fields,
31};
32use parse_rust_storage::{
33 AddFieldOutcome, ClassSchema, Clause, Comparison, Constraint, FieldType, Query, QueryOptions,
34 SortDirection, StorageAdapter, UpdateValue, DEFAULT_LIMIT,
35};
36
37use crate::acl::{lower_acl, raise_acl, AclScope};
38use crate::clp::{
39 adds_field, apply_pointer_permissions, deny_protected_fields, filter_sensitive_data,
40 plan_protected_fields, validate_permission, PermissionOptions, PointerPermOutcome,
41 ProtectedFieldPlan, WriteAction,
42};
43use crate::include;
44use crate::query_parse::{ParsedClause, ParsedWhere};
45use crate::relations::{self, RelationConstraint};
46use crate::snapshot::SchemaSnapshot;
47use crate::write::{
48 as_plain_body, echo_response, echoed_keys, flatten_for_create, lower_update, WriteBody,
49};
50
51type BoxFut<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
54
55pub struct Ctx<'a, S: StorageAdapter> {
60 pub storage: &'a S,
61 pub snapshot: &'a SchemaSnapshot,
62 pub scope: &'a AclScope,
63 pub options: &'a PermissionOptions,
64 pub is_maintenance: bool,
78}
79
80impl<'a, S: StorageAdapter> Ctx<'a, S> {
81 pub fn new(
82 storage: &'a S,
83 snapshot: &'a SchemaSnapshot,
84 scope: &'a AclScope,
85 options: &'a PermissionOptions,
86 ) -> Self {
87 Self {
88 is_maintenance: false,
89 storage,
90 snapshot,
91 scope,
92 options,
93 }
94 }
95
96 pub fn maintenance(mut self, yes: bool) -> Self {
101 self.is_maintenance = yes;
102 self
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct FindOptions {
109 pub limit: Option<u32>,
110 pub skip: Option<u32>,
111 pub order: Vec<(String, SortDirection)>,
112 pub keys: Option<Vec<String>>,
113 pub exclude_keys: Option<Vec<String>>,
116 pub include: Vec<Vec<String>>,
119}
120
121impl Default for FindOptions {
122 fn default() -> Self {
123 Self {
124 limit: Some(DEFAULT_LIMIT),
125 skip: None,
126 order: Vec::new(),
127 keys: None,
128 exclude_keys: None,
129 include: Vec::new(),
130 }
131 }
132}
133
134#[derive(Debug, Clone)]
137pub struct CreateResponse {
138 pub object_id: String,
139 pub created_at: ParseDate,
140 pub echoed: ParseMap,
142}
143
144#[derive(Debug, Clone)]
146pub struct UpdateResponse {
147 pub updated_at: ParseDate,
148 pub echoed: ParseMap,
149}
150
151fn object_not_found() -> ParseError {
156 ParseError::new(ErrorCode::ObjectNotFound, "Object not found.")
157}
158
159pub async fn find<S: StorageAdapter>(
165 ctx: &Ctx<'_, S>,
166 class_name: &str,
167 where_: ParsedWhere,
168 options: FindOptions,
169) -> Result<Vec<ParseMap>, ParseError> {
170 let op = derived_op(&where_);
174 let mut results = find_core(
175 ctx,
176 class_name,
177 where_,
178 options.clone(),
179 op,
180 ReadMethod::Find,
183 )
184 .await?;
185 expand_includes(ctx, &mut results, &options).await?;
186 Ok(results)
187}
188
189pub async fn get<S: StorageAdapter>(
191 ctx: &Ctx<'_, S>,
192 class_name: &str,
193 object_id: &str,
194 options: FindOptions,
195) -> Result<ParseMap, ParseError> {
196 let options = FindOptions {
197 limit: Some(1),
198 ..options
199 };
200 let mut results = find_core(
201 ctx,
202 class_name,
203 pinned_where(object_id),
204 options.clone(),
205 Operation::Get,
206 ReadMethod::Get,
207 )
208 .await?;
209 expand_includes(ctx, &mut results, &options).await?;
210 results.into_iter().next().ok_or_else(object_not_found)
211}
212
213pub async fn count<S: StorageAdapter>(
215 ctx: &Ctx<'_, S>,
216 class_name: &str,
217 where_: ParsedWhere,
218) -> Result<u64, ParseError> {
219 let schema = ctx.snapshot.get_or_default(class_name);
220 let plan = plan_read(
222 ctx,
223 class_name,
224 &schema,
225 where_,
226 &[],
227 Operation::Count,
228 ReadMethod::Find,
229 )
230 .await?;
231 let query = match plan {
232 ReadPlan::Denied => return Ok(0),
240 ReadPlan::Run { query, .. } => query,
241 };
242 if !ctx.snapshot.contains(class_name) {
243 return Ok(0);
244 }
245 ctx.storage.count(&schema, &query).await
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262enum ReadMethod {
263 Get,
264 Find,
265}
266
267impl ReadMethod {
268 fn as_str(self) -> &'static str {
269 match self {
270 ReadMethod::Get => "get",
271 ReadMethod::Find => "find",
272 }
273 }
274}
275
276fn derived_op(where_: &ParsedWhere) -> Operation {
278 if where_.clauses.len() == 1 && where_.pinned_object_id().is_some() {
279 Operation::Get
280 } else {
281 Operation::Find
282 }
283}
284
285fn pinned_where(object_id: &str) -> ParsedWhere {
286 let mut where_ = ParsedWhere::default();
287 where_.push(ParsedClause::Field(Constraint::equal(
288 "objectId",
289 ParseValue::String(object_id.to_string()),
290 )));
291 where_
292}
293
294enum ReadPlan {
296 Run {
297 query: Query,
298 protected: Option<ProtectedFieldPlan>,
299 order: Vec<(String, SortDirection)>,
300 },
301 Denied,
303}
304
305pub const SESSION_CLASS: &str = "_Session";
307pub const USER_CLASS: &str = "_User";
309
310fn narrow_sessions(
328 where_: &mut ParsedWhere,
329 class_name: &str,
330 scope: &AclScope,
331 detail: parse_rust_core::ErrorDetail,
332) -> Result<(), ParseError> {
333 if class_name != SESSION_CLASS || scope.is_master() {
334 return Ok(());
335 }
336 let Some(user_id) = scope.user_id() else {
337 return Err(ParseError::permission_denied(
340 ErrorCode::InvalidSessionToken,
341 "Invalid session token",
342 detail,
343 ));
344 };
345 let mine = ParsedWhere {
346 clauses: vec![ParsedClause::Field(Constraint::equal(
347 "user",
348 ParseValue::Pointer {
349 class_name: USER_CLASS.to_string(),
350 object_id: user_id.to_string(),
351 },
352 ))],
353 };
354 if where_.is_empty() {
362 *where_ = mine;
363 } else {
364 let client = std::mem::take(where_);
365 where_.push(ParsedClause::And(vec![client, mine]));
366 }
367 Ok(())
368}
369
370fn plan_read<'a, S: StorageAdapter>(
372 ctx: &'a Ctx<'a, S>,
373 class_name: &'a str,
374 schema: &'a ClassSchema,
375 mut where_: ParsedWhere,
376 order: &'a [(String, SortDirection)],
377 op: Operation,
378 method: ReadMethod,
379) -> BoxFut<'a, Result<ReadPlan, ParseError>> {
380 Box::pin(async move {
381 let clp = ctx.snapshot.clp(class_name);
382 let acl_group = ctx.scope.acl_group();
383 let master = ctx.scope.is_master();
384
385 crate::class_security::enforce_class_security(
390 class_name,
391 master,
392 method.as_str(),
395 ctx.options.error_detail,
396 )?;
397 narrow_sessions(&mut where_, class_name, ctx.scope, ctx.options.error_detail)?;
398
399 let protected = if master {
401 None
402 } else {
403 plan_protected_fields(
404 class_name,
405 clp,
406 ctx.scope,
407 where_.pinned_object_id(),
408 ctx.options,
409 )
410 };
411
412 if !master {
414 deny_protected_fields(
415 protected.as_ref(),
416 class_name,
417 &where_,
418 order,
419 ctx.options.error_detail,
420 )?;
421 }
422
423 let order = validate_sort(schema, class_name, order)?;
425
426 if !master {
428 validate_permission(
429 clp,
430 class_name,
431 &acl_group,
432 op,
433 None,
434 ctx.options.error_detail,
435 )?;
436 }
437
438 crate::query_parse::validate_query_keys(&where_, master)?;
444
445 let mut query = resolve_where(ctx, class_name, schema, where_).await?;
447
448 if !master {
450 match apply_pointer_permissions(schema, clp, op, &acl_group, &query)? {
451 PointerPermOutcome::Unconstrained => {}
452 PointerPermOutcome::Constrained(narrowed) => query = narrowed,
453 PointerPermOutcome::DenyAll => return Ok(ReadPlan::Denied),
455 }
456 }
457
458 let constraint = match op {
466 Operation::Update | Operation::Delete => ctx.scope.write_constraint(),
467 _ => ctx.scope.read_constraint(),
468 };
469 if let Some(constraint) = constraint {
470 query.push_constraint(constraint);
471 }
472
473 Ok(ReadPlan::Run {
474 query,
475 protected,
476 order,
477 })
478 })
479}
480
481fn find_core<'a, S: StorageAdapter>(
486 ctx: &'a Ctx<'a, S>,
487 class_name: &'a str,
488 where_: ParsedWhere,
489 options: FindOptions,
490 op: Operation,
491 method: ReadMethod,
492) -> BoxFut<'a, Result<Vec<ParseMap>, ParseError>> {
493 Box::pin(async move {
494 let schema = ctx.snapshot.get_or_default(class_name);
495 let plan = plan_read(ctx, class_name, &schema, where_, &options.order, op, method).await?;
496 let (query, protected, order) = match plan {
497 ReadPlan::Denied => {
498 return if op == Operation::Get {
500 Err(object_not_found())
501 } else {
502 Ok(Vec::new())
503 };
504 }
505 ReadPlan::Run {
506 query,
507 protected,
508 order,
509 } => (query, protected, order),
510 };
511
512 if !ctx.snapshot.contains(class_name) {
513 validate_client_class_creation(ctx, class_name, false)?;
519 return Ok(Vec::new());
520 }
521
522 let query_options = QueryOptions {
523 limit: options.limit,
524 skip: options.skip,
525 order,
526 keys: projection(&schema, &options),
527 case_insensitive: false,
528 };
529 let rows = ctx.storage.find(&schema, &query, &query_options).await?;
530
531 let is_read = matches!(op, Operation::Get | Operation::Find);
533 Ok(rows
534 .into_iter()
535 .map(|row| {
536 let mut row = raise_acl(row);
537 filter_sensitive_data(
538 &mut row,
539 class_name,
540 ctx.scope,
541 protected.as_ref(),
542 is_read,
543 ctx.options,
544 );
545 row
546 })
547 .collect())
548 })
549}
550
551fn projection(schema: &ClassSchema, options: &FindOptions) -> Option<Vec<String>> {
556 const ALWAYS: [&str; 4] = ["objectId", "createdAt", "updatedAt", "ACL"];
558 match (&options.keys, &options.exclude_keys) {
559 (None, None) => None,
560 (Some(keys), None) => {
561 let mut out = keys.clone();
562 for key in ALWAYS {
563 if !out.iter().any(|k| k == key) {
564 out.push(key.to_string());
565 }
566 }
567 Some(out)
568 }
569 (keys, Some(exclude)) => {
570 let exclude: Vec<&String> = exclude
572 .iter()
573 .filter(|k| !ALWAYS.contains(&k.as_str()))
574 .collect();
575 let base: Vec<String> = match keys {
576 Some(keys) => {
577 let mut out = keys.clone();
578 for key in ALWAYS {
579 if !out.iter().any(|k| k == key) {
580 out.push(key.to_string());
581 }
582 }
583 out
584 }
585 None => schema.fields.keys().cloned().collect(),
586 };
587 Some(base.into_iter().filter(|k| !exclude.contains(&k)).collect())
588 }
589 }
590}
591
592fn resolve_where<'a, S: StorageAdapter>(
594 ctx: &'a Ctx<'a, S>,
595 class_name: &'a str,
596 schema: &'a ClassSchema,
597 where_: ParsedWhere,
598) -> BoxFut<'a, Result<Query, ParseError>> {
599 Box::pin(async move {
600 let mut query = Query::new();
601 let mut relation_groups: IndexMap<String, Vec<Comparison>> = IndexMap::new();
614 for clause in where_.clauses {
615 match clause {
616 ParsedClause::Field(constraint) => {
617 match schema.field(&constraint.field) {
618 Some(FieldType::Relation { .. }) => {
621 relation_groups
622 .entry(constraint.field.clone())
623 .or_default()
624 .push(constraint.comparison);
625 }
626 _ => query.push_constraint(constraint),
627 }
628 }
629 ParsedClause::RelatedTo {
630 class_name: owning_class,
631 object_id: owning_id,
632 key,
633 } => {
634 let outcome = resolve_related_to(ctx, &owning_class, &owning_id, &key).await?;
635 relations::add_in_object_ids(&mut query, outcome.ids());
638 }
639 ParsedClause::Or(branches) => {
640 query.push(Clause::Or(
641 resolve_branches(ctx, class_name, schema, branches).await?,
642 ));
643 }
644 ParsedClause::And(branches) => {
645 query.push(Clause::And(
646 resolve_branches(ctx, class_name, schema, branches).await?,
647 ));
648 }
649 ParsedClause::Nor(branches) => {
650 query.push(Clause::Nor(
667 resolve_branches(ctx, class_name, schema, branches).await?,
668 ));
669 }
670 }
671 }
672
673 for (field, comparisons) in relation_groups {
676 for constraint in relations::relation_constraints_for(&comparisons)? {
677 match constraint {
678 RelationConstraint::OwnersOf(ids) => {
679 let owners =
680 relations::owning_ids(ctx.storage, class_name, &field, &ids).await?;
681 relations::add_in_object_ids(&mut query, &owners);
682 }
683 RelationConstraint::NotOwnersOf(ids) => {
684 let owners =
685 relations::owning_ids(ctx.storage, class_name, &field, &ids).await?;
686 relations::add_not_in_object_ids(&mut query, &owners);
687 }
688 }
689 }
690 }
691 Ok(query)
692 })
693}
694
695async fn resolve_branches<S: StorageAdapter>(
696 ctx: &Ctx<'_, S>,
697 class_name: &str,
698 schema: &ClassSchema,
699 branches: Vec<ParsedWhere>,
700) -> Result<Vec<Query>, ParseError> {
701 let mut out = Vec::with_capacity(branches.len());
702 for branch in branches {
703 out.push(resolve_where(ctx, class_name, schema, branch).await?);
704 }
705 Ok(out)
706}
707
708async fn resolve_related_to<S: StorageAdapter>(
710 ctx: &Ctx<'_, S>,
711 owning_class: &str,
712 owning_id: &str,
713 key: &str,
714) -> Result<relations::RelatedToOutcome, ParseError> {
715 if ctx.scope.is_master() {
716 let ids = relations::related_ids(ctx.storage, owning_class, key, owning_id).await?;
717 return Ok(relations::RelatedToOutcome::Ids(ids));
718 }
719
720 let owning_protected = plan_protected_fields(
721 owning_class,
722 ctx.snapshot.clp(owning_class),
723 ctx.scope,
724 Some(owning_id),
725 ctx.options,
726 )
727 .map(|p| p.strip)
728 .unwrap_or_default();
729
730 let authorized = relations::authorize_related_to(
731 owning_class,
732 key,
733 &owning_protected,
734 ctx.options.error_detail,
735 || async move {
736 let options = FindOptions {
739 limit: Some(1),
740 keys: Some(vec!["objectId".to_string()]),
741 ..Default::default()
742 };
743 match find_core(
744 ctx,
745 owning_class,
746 pinned_where(owning_id),
747 options,
748 Operation::Get,
749 ReadMethod::Get,
750 )
751 .await
752 {
753 Ok(rows) => Ok(!rows.is_empty()),
754 Err(e)
755 if e.code == ErrorCode::OperationForbidden
756 || e.code == ErrorCode::ObjectNotFound =>
757 {
758 Ok(false)
759 }
760 Err(e) => Err(e),
761 }
762 },
763 )
764 .await?;
765
766 if !authorized {
767 return Ok(relations::RelatedToOutcome::DeniedYieldEmpty);
768 }
769 let ids = relations::related_ids(ctx.storage, owning_class, key, owning_id).await?;
770 Ok(relations::RelatedToOutcome::Ids(ids))
771}
772
773fn validate_sort(
775 schema: &ClassSchema,
776 class_name: &str,
777 order: &[(String, SortDirection)],
778) -> Result<Vec<(String, SortDirection)>, ParseError> {
779 let mut out = Vec::new();
780 for (field, direction) in order {
781 if is_auth_data_id_path(field) {
782 return Err(ParseError::invalid_key_name(format!(
783 "Cannot sort by {field}"
784 )));
785 }
786 let root = field.split('.').next().unwrap_or(field);
787 if !field_name_is_valid(root, class_name) {
788 return Err(ParseError::invalid_key_name(format!(
789 "Invalid field name: {field}."
790 )));
791 }
792 if schema.field(root).is_none() && field != "score" {
795 continue;
796 }
797 out.push((field.clone(), *direction));
798 }
799 Ok(out)
800}
801
802fn is_auth_data_id_path(field: &str) -> bool {
804 let parts: Vec<&str> = field.split('.').collect();
805 matches!(parts.as_slice(), ["authData", provider, "id"]
806 if !provider.is_empty()
807 && provider.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
808}
809
810async fn expand_includes<S: StorageAdapter>(
812 ctx: &Ctx<'_, S>,
813 results: &mut [ParseMap],
814 options: &FindOptions,
815) -> Result<(), ParseError> {
816 if options.include.is_empty() || results.is_empty() {
817 return Ok(());
818 }
819 let keys = options.keys.clone().unwrap_or_default();
820 let exclude_keys = options.exclude_keys.clone().unwrap_or_default();
821
822 for path in &options.include {
823 let by_class = include::collect_pointers(results, path);
824 if by_class.is_empty() {
825 continue;
826 }
827 let mut fetched: IndexMap<String, ParseMap> = IndexMap::new();
828 for (target_class, ids) in by_class.iter() {
829 let mut where_ = ParsedWhere::default();
830 let method = if ids.len() == 1 {
840 ReadMethod::Get
841 } else {
842 ReadMethod::Find
843 };
844 let constraint = if ids.len() == 1 {
845 Constraint::equal("objectId", ParseValue::String(ids[0].clone()))
846 } else {
847 Constraint::one_of(
848 "objectId",
849 ids.iter()
850 .map(|id| ParseValue::String(id.clone()))
851 .collect(),
852 )
853 };
854 where_.push(ParsedClause::Field(constraint));
855
856 let nested = FindOptions {
857 limit: Some(ids.len() as u32),
858 skip: None,
859 order: Vec::new(),
860 keys: include::keys_for_path(&keys, path),
861 exclude_keys: include::exclude_keys_for_path(&exclude_keys, path),
862 include: Vec::new(),
863 };
864
865 let rows = find_core(ctx, target_class, where_, nested, Operation::Get, method).await?;
870 for mut row in rows {
871 let Some(ParseValue::String(id)) = row.get("objectId").cloned() else {
872 continue;
873 };
874 include::shape_included(&mut row, target_class, ctx.scope.is_master());
875 fetched.insert(id, row);
876 }
877 }
878 include::graft(results, path, &fetched);
879 }
880 Ok(())
881}
882
883pub async fn create<S: StorageAdapter>(
889 ctx: &Ctx<'_, S>,
890 class_name: &str,
891 mut body: WriteBody,
892) -> Result<CreateResponse, ParseError> {
893 let class_exists = ctx.snapshot.contains(class_name);
894 let mut schema = ctx.snapshot.resolve_for_write(class_name);
895 let clp = ctx.snapshot.clp(class_name);
896 let acl_group = ctx.scope.acl_group();
897 let master = ctx.scope.is_master();
898
899 validate_required_columns(class_name, &as_plain_body(&body), false)?;
904
905 if !master
907 && adds_field(
908 &schema,
909 class_exists,
910 body.keys().map(String::as_str),
911 |key| matches!(body.get(key), Some(FieldWrite::Op(Op::Delete))),
912 )
913 {
914 validate_permission(
915 clp,
916 class_name,
917 &acl_group,
918 Operation::AddField,
919 Some(WriteAction::Create),
920 ctx.options.error_detail,
921 )?;
922 }
923
924 if !master {
926 validate_permission(
927 clp,
928 class_name,
929 &acl_group,
930 Operation::Create,
931 Some(WriteAction::Create),
932 ctx.options.error_detail,
933 )?;
934 }
935
936 ensure_class_exists(ctx, class_name, class_exists).await?;
954
955 let object_id = match body.get("objectId") {
970 None => new_object_id(),
971 Some(FieldWrite::Value(v)) if !parse_rust_core::is_js_truthy(v) => new_object_id(),
972 Some(FieldWrite::Value(ParseValue::String(id))) => id.clone(),
973 Some(other) => {
974 let got = match other {
975 FieldWrite::Value(v) => infer_type(v),
976 FieldWrite::Op(op) => infer_op_type(op)?,
977 };
978 return Err(match got {
982 Some(got) => schema_mismatch(class_name, "objectId", &FieldType::String, &got),
983 None => ParseError::invalid_json("objectId is an invalid field name."),
986 });
987 }
988 };
989 let now = ParseDate::now();
990 body.insert(
991 "objectId".to_string(),
992 FieldWrite::Value(ParseValue::String(object_id.clone())),
993 );
994 body.insert(
995 "createdAt".to_string(),
996 FieldWrite::Value(ParseValue::Date(now)),
997 );
998 body.insert(
999 "updatedAt".to_string(),
1000 FieldWrite::Value(ParseValue::Date(now)),
1001 );
1002
1003 let delta = validate_write_fields(&schema, &body)?;
1010 reserve_schema(ctx, class_name, &schema, &delta.added).await?;
1011 apply(&mut schema, &delta);
1012
1013 let relation_updates = relations::collect_relation_updates(&mut body);
1014
1015 let row = lower_acl(flatten_for_create(&body)?);
1018 ctx.storage.create(&schema, &row).await?;
1019
1020 relations::apply_relation_updates(ctx.storage, class_name, &object_id, &relation_updates)
1021 .await?;
1022
1023 Ok(CreateResponse {
1024 object_id,
1025 created_at: now,
1026 echoed: echo_response(&body, Some(&row)),
1027 })
1028}
1029
1030pub async fn update<S: StorageAdapter>(
1032 ctx: &Ctx<'_, S>,
1033 class_name: &str,
1034 object_id: &str,
1035 mut body: WriteBody,
1036) -> Result<UpdateResponse, ParseError> {
1037 let class_exists = ctx.snapshot.contains(class_name);
1038 let mut schema = ctx.snapshot.resolve_for_write(class_name);
1039 let clp = ctx.snapshot.clp(class_name);
1040 let acl_group = ctx.scope.acl_group();
1041 let master = ctx.scope.is_master();
1042
1043 body.shift_remove("objectId");
1045 body.shift_remove("createdAt");
1046
1047 validate_required_columns(class_name, &as_plain_body(&body), true)?;
1048
1049 let introduces_field = adds_field(
1050 &schema,
1051 class_exists,
1052 body.keys().map(String::as_str),
1053 |key| matches!(body.get(key), Some(FieldWrite::Op(Op::Delete))),
1054 );
1055 if !master && introduces_field {
1056 validate_permission(
1057 clp,
1058 class_name,
1059 &acl_group,
1060 Operation::AddField,
1061 Some(WriteAction::Update),
1062 ctx.options.error_detail,
1063 )?;
1064 }
1065
1066 if !master {
1067 validate_permission(
1068 clp,
1069 class_name,
1070 &acl_group,
1071 Operation::Update,
1072 Some(WriteAction::Update),
1073 ctx.options.error_detail,
1074 )?;
1075 }
1076
1077 let mut query = Query::from_constraints(vec![Constraint::equal(
1078 "objectId",
1079 ParseValue::String(object_id.to_string()),
1080 )]);
1081 if !master {
1082 match apply_pointer_permissions(&schema, clp, Operation::Update, &acl_group, &query)? {
1083 PointerPermOutcome::Unconstrained => {}
1084 PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1085 PointerPermOutcome::DenyAll => return Err(object_not_found()),
1089 }
1090 if introduces_field {
1091 match apply_pointer_permissions(&schema, clp, Operation::AddField, &acl_group, &query)?
1094 {
1095 PointerPermOutcome::Unconstrained => {}
1096 PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1097 PointerPermOutcome::DenyAll => return Err(object_not_found()),
1098 }
1099 }
1100 if let Some(constraint) = ctx.scope.write_constraint() {
1101 query.push_constraint(constraint);
1102 }
1103 }
1104
1105 let updated_at = ParseDate::now();
1106 body.insert(
1107 "updatedAt".to_string(),
1108 FieldWrite::Value(ParseValue::Date(updated_at)),
1109 );
1110
1111 ensure_class_exists(ctx, class_name, class_exists).await?;
1129 let delta = validate_write_fields(&schema, &body)?;
1130 reserve_schema(ctx, class_name, &schema, &delta.added).await?;
1131 apply(&mut schema, &delta);
1132
1133 let relation_updates = relations::collect_relation_updates(&mut body);
1134
1135 let mut update = lower_update(&body)?;
1136 lower_acl_into_update(&mut body, &mut update);
1137
1138 let echoed = if echoed_keys(&body).is_empty() {
1140 let matched = ctx.storage.update(&schema, &query, &update).await?;
1141 if matched == 0 {
1142 return Err(object_not_found());
1143 }
1144 ParseMap::new()
1145 } else {
1146 let row = ctx
1147 .storage
1148 .update_one_returning(&schema, &query, &update)
1149 .await?;
1150 let Some(row) = row else {
1151 return Err(object_not_found());
1152 };
1153 echo_response(&body, Some(&row))
1154 };
1155
1156 relations::apply_relation_updates(ctx.storage, class_name, object_id, &relation_updates)
1157 .await?;
1158
1159 Ok(UpdateResponse { updated_at, echoed })
1160}
1161
1162fn lower_acl_into_update(body: &mut WriteBody, update: &mut parse_rust_storage::Update) {
1168 let Some(write) = body.shift_remove("ACL") else {
1169 return;
1170 };
1171 update.shift_remove("ACL");
1172 let value = match write {
1173 FieldWrite::Value(v) if !parse_rust_core::is_js_truthy(&v) => return,
1184 FieldWrite::Value(value) => value,
1185 FieldWrite::Op(_) => ParseValue::Object(ParseMap::new()),
1188 };
1189 let mut carrier = ParseMap::new();
1190 carrier.insert("ACL".to_string(), value);
1191 let lowered = lower_acl(carrier);
1192 for key in ["_rperm", "_wperm"] {
1193 let value = lowered
1194 .get(key)
1195 .cloned()
1196 .unwrap_or(ParseValue::Array(Vec::new()));
1197 update.insert(key.to_string(), UpdateValue::Set(value));
1198 }
1199}
1200
1201pub async fn delete<S: StorageAdapter>(
1203 ctx: &Ctx<'_, S>,
1204 class_name: &str,
1205 object_id: &str,
1206) -> Result<(), ParseError> {
1207 let schema = ctx.snapshot.get_or_default(class_name);
1208 let clp = ctx.snapshot.clp(class_name);
1209 let acl_group = ctx.scope.acl_group();
1210 let master = ctx.scope.is_master();
1211
1212 if !master {
1213 validate_permission(
1214 clp,
1215 class_name,
1216 &acl_group,
1217 Operation::Delete,
1218 None,
1219 ctx.options.error_detail,
1220 )?;
1221 }
1222
1223 let mut query = Query::from_constraints(vec![Constraint::equal(
1224 "objectId",
1225 ParseValue::String(object_id.to_string()),
1226 )]);
1227 if !master {
1228 match apply_pointer_permissions(&schema, clp, Operation::Delete, &acl_group, &query)? {
1229 PointerPermOutcome::Unconstrained => {}
1230 PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1231 PointerPermOutcome::DenyAll => return Err(object_not_found()),
1233 }
1234 if let Some(constraint) = ctx.scope.write_constraint() {
1235 query.push_constraint(constraint);
1236 }
1237 }
1238
1239 let deleted = ctx.storage.delete(&schema, &query).await?;
1240 if deleted == 0 {
1241 return Err(object_not_found());
1242 }
1243 Ok(())
1244}
1245
1246async fn ensure_class_exists<S: StorageAdapter>(
1280 ctx: &Ctx<'_, S>,
1281 class_name: &str,
1282 class_exists: bool,
1283) -> Result<(), ParseError> {
1284 if class_exists {
1285 return Ok(());
1286 }
1287 validate_client_class_creation(ctx, class_name, true)?;
1288 if !parse_rust_schema::class_name_is_valid(class_name) {
1289 return Err(ParseError::invalid_json(
1290 "schema class name does not revalidate",
1291 ));
1292 }
1293 ctx.storage.upsert_schema(&default_schema(class_name)).await
1294}
1295
1296fn validate_client_class_creation<S: StorageAdapter>(
1312 ctx: &Ctx<'_, S>,
1313 class_name: &str,
1314 maintenance_is_exempt: bool,
1315) -> Result<(), ParseError> {
1316 let privileged = if maintenance_is_exempt {
1322 ctx.scope.is_master()
1323 } else {
1324 ctx.scope.is_master() && !ctx.is_maintenance
1325 };
1326 if ctx.options.allow_client_class_creation
1327 || privileged
1328 || parse_rust_schema::SYSTEM_CLASSES.contains(&class_name)
1329 {
1330 return Ok(());
1331 }
1332 Err(ParseError::permission_denied(
1335 ErrorCode::OperationForbidden,
1336 format!("This user is not allowed to access non-existent class: {class_name}"),
1337 ctx.options.error_detail,
1338 ))
1339}
1340
1341async fn reserve_schema<S: StorageAdapter>(
1342 ctx: &Ctx<'_, S>,
1343 class_name: &str,
1344 schema: &ClassSchema,
1345 added: &[(String, FieldType)],
1346) -> Result<(), ParseError> {
1347 for (field_name, field_type) in added {
1348 match ctx
1349 .storage
1350 .reserve_field(class_name, field_name, field_type, None)
1353 .await?
1354 {
1355 AddFieldOutcome::Added | AddFieldOutcome::AlreadyPresentSameType => {}
1356 AddFieldOutcome::Conflict { existing } => {
1357 return Err(schema_mismatch(
1361 &schema.class_name,
1362 field_name,
1363 &existing,
1364 field_type,
1365 ));
1366 }
1367 }
1368 }
1369 Ok(())
1370}
1371
1372#[cfg(test)]
1373mod tests {
1374 use super::*;
1375 use crate::query_parse::parse_where;
1376 use crate::testing::FakeStorage;
1377 use crate::write::decode_write_body;
1378 use parse_rust_core::op::OpPath;
1379 use parse_rust_core::ClassLevelPermissions;
1380
1381 fn clp(json: &str) -> ClassLevelPermissions {
1382 let value = parse_rust_core::classify(
1383 serde_json::from_str(json).expect("test literal must be valid JSON"),
1384 )
1385 .expect("classify");
1386 match value {
1387 ParseValue::Object(m) => ClassLevelPermissions::from_map(m),
1388 _ => panic!("expected an object"),
1389 }
1390 }
1391
1392 fn where_(json: &str) -> ParsedWhere {
1393 parse_where(&serde_json::from_str(json).expect("test literal")).expect("parse")
1394 }
1395
1396 fn body(json: &str, path: OpPath) -> WriteBody {
1397 decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
1398 }
1399
1400 fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
1401 let mut m = ParseMap::new();
1402 for (k, v) in pairs {
1403 m.insert(k.to_string(), v);
1404 }
1405 m
1406 }
1407
1408 fn strings(values: &[&str]) -> ParseValue {
1409 ParseValue::Array(
1410 values
1411 .iter()
1412 .map(|v| ParseValue::String((*v).to_string()))
1413 .collect(),
1414 )
1415 }
1416
1417 fn pointer(class: &str, id: &str) -> ParseValue {
1418 ParseValue::Pointer {
1419 class_name: class.to_string(),
1420 object_id: id.to_string(),
1421 }
1422 }
1423
1424 async fn snapshot(storage: &FakeStorage) -> SchemaSnapshot {
1425 SchemaSnapshot::load(storage).await.expect("snapshot")
1426 }
1427
1428 fn opts() -> PermissionOptions {
1431 PermissionOptions::default()
1432 }
1433
1434 fn disclosing_opts() -> PermissionOptions {
1437 PermissionOptions {
1438 error_detail: parse_rust_core::ErrorDetail::Disclosed,
1439 ..PermissionOptions::default()
1440 }
1441 }
1442
1443 #[tokio::test]
1449 async fn create_runs_the_clp_gate() {
1450 let storage = FakeStorage::new().with_schema(
1451 default_schema("Post").with_clp(clp(r#"{"create":{"role:Writers":true}}"#)),
1452 );
1453 let snap = snapshot(&storage).await;
1454 let options = opts();
1455
1456 let anon = AclScope::Anonymous;
1457 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1458 let e = create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1459 .await
1460 .unwrap_err();
1461 assert_eq!(e.code, ErrorCode::OperationForbidden);
1462 assert_eq!(e.message, "Permission denied");
1463 assert!(storage.rows("Post").is_empty(), "nothing was written");
1464
1465 let disclosing = disclosing_opts();
1466 let ctx = Ctx::new(&storage, &snap, &anon, &disclosing);
1467 assert_eq!(
1468 create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1469 .await
1470 .unwrap_err()
1471 .message,
1472 "Permission denied for action create on class Post."
1473 );
1474 assert!(storage.rows("Post").is_empty(), "nothing was written");
1475
1476 let writer = AclScope::user("u1", vec!["Writers".into()]).expect("scope");
1477 let ctx = Ctx::new(&storage, &snap, &writer, &options);
1478 assert!(
1479 create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1480 .await
1481 .is_ok()
1482 );
1483 }
1484
1485 #[tokio::test]
1487 async fn a_class_with_no_clp_is_unrestricted() {
1488 let storage = FakeStorage::new().with_schema(default_schema("Post"));
1489 let snap = snapshot(&storage).await;
1490 let options = opts();
1491 let anon = AclScope::Anonymous;
1492 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1493 assert!(
1494 create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1495 .await
1496 .is_ok()
1497 );
1498 assert!(
1499 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1500 .await
1501 .is_ok()
1502 );
1503 }
1504
1505 #[tokio::test]
1507 async fn requires_authentication_denies_a_read_with_object_not_found() {
1508 let storage = FakeStorage::new()
1509 .with_schema(
1510 default_schema("Post").with_clp(clp(r#"{"find":{"requiresAuthentication":true}}"#)),
1511 )
1512 .with_row(
1513 "Post",
1514 row(vec![("objectId", ParseValue::String("p1".into()))]),
1515 );
1516 let snap = snapshot(&storage).await;
1517 let options = opts();
1518
1519 let anon = AclScope::Anonymous;
1520 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1521 let e = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1522 .await
1523 .unwrap_err();
1524 assert_eq!(e.code, ErrorCode::ObjectNotFound);
1525 assert_eq!(e.message, "Permission denied");
1526
1527 let disclosing = disclosing_opts();
1528 let disclosed = Ctx::new(&storage, &snap, &anon, &disclosing);
1529 let e = find(
1530 &disclosed,
1531 "Post",
1532 ParsedWhere::default(),
1533 FindOptions::default(),
1534 )
1535 .await
1536 .unwrap_err();
1537 assert_eq!(e.code, ErrorCode::ObjectNotFound);
1538 assert_eq!(
1539 e.message,
1540 "Permission denied, user needs to be authenticated."
1541 );
1542
1543 let user = AclScope::user("u1", vec![]).expect("scope");
1544 let ctx = Ctx::new(&storage, &snap, &user, &options);
1545 assert_eq!(
1546 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1547 .await
1548 .expect("allowed")
1549 .len(),
1550 1
1551 );
1552 }
1553
1554 fn pointer_perm_storage() -> FakeStorage {
1559 let clp_json = r#"{
1560 "find":{"pointerFields":["owner"]},
1561 "get":{"pointerFields":["owner"]},
1562 "count":{"pointerFields":["owner"]},
1563 "update":{"pointerFields":["owner"]},
1564 "delete":{"pointerFields":["owner"]},
1565 "create":{"*":true}
1566 }"#;
1567 FakeStorage::new()
1568 .with_schema(
1569 default_schema("Post")
1570 .with_field(
1571 "owner",
1572 FieldType::Pointer {
1573 target_class: "_User".into(),
1574 },
1575 )
1576 .with_field("title", FieldType::String)
1577 .with_clp(clp(clp_json)),
1578 )
1579 .with_row(
1580 "Post",
1581 row(vec![
1582 ("objectId", ParseValue::String("p1".into())),
1583 ("owner", pointer("_User", "u1")),
1584 ("title", ParseValue::String("mine".into())),
1585 ]),
1586 )
1587 .with_row(
1588 "Post",
1589 row(vec![
1590 ("objectId", ParseValue::String("p2".into())),
1591 ("owner", pointer("_User", "u2")),
1592 ("title", ParseValue::String("theirs".into())),
1593 ]),
1594 )
1595 }
1596
1597 #[tokio::test]
1601 async fn an_anonymous_caller_gets_nothing_from_a_pointer_permission_class() {
1602 let storage = pointer_perm_storage();
1603 let snap = snapshot(&storage).await;
1604 let options = opts();
1605 let anon = AclScope::Anonymous;
1606 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1607
1608 assert!(
1609 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1610 .await
1611 .expect("find resolves empty rather than erroring")
1612 .is_empty()
1613 );
1614 assert_eq!(
1615 get(&ctx, "Post", "p1", FindOptions::default())
1616 .await
1617 .unwrap_err()
1618 .code,
1619 ErrorCode::ObjectNotFound
1620 );
1621 assert_eq!(
1622 count(&ctx, "Post", ParsedWhere::default())
1623 .await
1624 .expect("count resolves"),
1625 0
1626 );
1627 assert_eq!(
1628 update(&ctx, "Post", "p1", body(r#"{"title":"x"}"#, OpPath::Update))
1629 .await
1630 .unwrap_err()
1631 .code,
1632 ErrorCode::ObjectNotFound
1633 );
1634 assert_eq!(
1635 delete(&ctx, "Post", "p1").await.unwrap_err().code,
1636 ErrorCode::ObjectNotFound
1637 );
1638 assert_eq!(storage.rows("Post").len(), 2, "nothing was deleted");
1639 }
1640
1641 #[tokio::test]
1642 async fn a_pointer_permission_narrows_a_user_to_their_own_rows() {
1643 let storage = pointer_perm_storage();
1644 let snap = snapshot(&storage).await;
1645 let options = opts();
1646 let u1 = AclScope::user("u1", vec![]).expect("scope");
1647 let ctx = Ctx::new(&storage, &snap, &u1, &options);
1648
1649 let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1650 .await
1651 .expect("find");
1652 assert_eq!(results.len(), 1);
1653 assert!(matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "p1"));
1654
1655 assert!(get(&ctx, "Post", "p1", FindOptions::default())
1656 .await
1657 .is_ok());
1658 assert_eq!(
1659 get(&ctx, "Post", "p2", FindOptions::default())
1660 .await
1661 .unwrap_err()
1662 .code,
1663 ErrorCode::ObjectNotFound
1664 );
1665 assert_eq!(
1666 count(&ctx, "Post", ParsedWhere::default())
1667 .await
1668 .expect("count"),
1669 1
1670 );
1671 assert!(
1672 update(&ctx, "Post", "p1", body(r#"{"title":"x"}"#, OpPath::Update))
1673 .await
1674 .is_ok()
1675 );
1676 assert_eq!(
1677 update(&ctx, "Post", "p2", body(r#"{"title":"x"}"#, OpPath::Update))
1678 .await
1679 .unwrap_err()
1680 .code,
1681 ErrorCode::ObjectNotFound
1682 );
1683 }
1684
1685 fn protected_storage() -> FakeStorage {
1690 FakeStorage::new()
1691 .with_schema(
1692 default_schema("Post")
1693 .with_field("secret", FieldType::String)
1694 .with_field("title", FieldType::String)
1695 .with_clp(clp(r#"{"protectedFields":{"*":["secret"]}}"#)),
1696 )
1697 .with_row(
1698 "Post",
1699 row(vec![
1700 ("objectId", ParseValue::String("p1".into())),
1701 ("title", ParseValue::String("t".into())),
1702 ("secret", ParseValue::String("s".into())),
1703 ]),
1704 )
1705 }
1706
1707 #[tokio::test]
1708 async fn a_protected_field_is_absent_from_a_read_and_present_for_master() {
1709 let storage = protected_storage();
1710 let snap = snapshot(&storage).await;
1711 let options = opts();
1712
1713 let anon = AclScope::Anonymous;
1714 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1715 let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1716 .await
1717 .expect("find");
1718 assert!(results[0].get("secret").is_none());
1719 assert!(results[0].get("title").is_some());
1720
1721 let master = AclScope::Unrestricted;
1722 let ctx = Ctx::new(&storage, &snap, &master, &options);
1723 let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1724 .await
1725 .expect("find");
1726 assert!(results[0].get("secret").is_some());
1727 }
1728
1729 #[tokio::test]
1731 async fn querying_or_ordering_by_a_protected_field_is_forbidden() {
1732 let storage = protected_storage();
1733 let snap = snapshot(&storage).await;
1734 let options = opts();
1735 let anon = AclScope::Anonymous;
1736 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1737
1738 let e = find(
1739 &ctx,
1740 "Post",
1741 where_(r#"{"secret":"s"}"#),
1742 FindOptions::default(),
1743 )
1744 .await
1745 .unwrap_err();
1746 assert_eq!(e.code, ErrorCode::OperationForbidden);
1747 assert_eq!(e.message, "Permission denied");
1748
1749 let disclosing = disclosing_opts();
1750 let disclosed = Ctx::new(&storage, &snap, &anon, &disclosing);
1751 assert_eq!(
1752 find(
1753 &disclosed,
1754 "Post",
1755 where_(r#"{"secret":"s"}"#),
1756 FindOptions::default()
1757 )
1758 .await
1759 .unwrap_err()
1760 .message,
1761 "This user is not allowed to query secret on class Post"
1762 );
1763
1764 assert_eq!(
1766 find(
1767 &ctx,
1768 "Post",
1769 where_(r#"{"$or":[{"secret.a":"s"}]}"#),
1770 FindOptions::default()
1771 )
1772 .await
1773 .unwrap_err()
1774 .code,
1775 ErrorCode::OperationForbidden
1776 );
1777
1778 let sorted = FindOptions {
1779 order: vec![("secret".to_string(), SortDirection::Ascending)],
1780 ..Default::default()
1781 };
1782 let e = find(&ctx, "Post", ParsedWhere::default(), sorted.clone())
1783 .await
1784 .unwrap_err();
1785 assert_eq!(e.code, ErrorCode::OperationForbidden);
1786 assert_eq!(e.message, "Permission denied");
1787 assert_eq!(
1788 find(&disclosed, "Post", ParsedWhere::default(), sorted)
1789 .await
1790 .unwrap_err()
1791 .message,
1792 "This user is not allowed to sort by secret on class Post"
1793 );
1794
1795 let master = AclScope::Unrestricted;
1797 let ctx = Ctx::new(&storage, &snap, &master, &options);
1798 assert!(find(
1799 &ctx,
1800 "Post",
1801 where_(r#"{"secret":"s"}"#),
1802 FindOptions::default()
1803 )
1804 .await
1805 .is_ok());
1806 }
1807
1808 #[tokio::test]
1813 async fn an_acl_hides_a_row_from_everyone_but_its_principals() {
1814 let storage = FakeStorage::new()
1815 .with_schema(default_schema("Post"))
1816 .with_row(
1817 "Post",
1818 row(vec![
1819 ("objectId", ParseValue::String("private".into())),
1820 ("_rperm", strings(&["u1", "role:Admins"])),
1821 ("_wperm", strings(&["u1"])),
1822 ]),
1823 )
1824 .with_row(
1825 "Post",
1826 row(vec![("objectId", ParseValue::String("public".into()))]),
1827 );
1828 let snap = snapshot(&storage).await;
1829 let options = opts();
1830
1831 let owner = AclScope::user("u1", vec![]).expect("scope");
1832 let ctx = Ctx::new(&storage, &snap, &owner, &options);
1833 assert_eq!(
1834 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1835 .await
1836 .expect("find")
1837 .len(),
1838 2,
1839 "assert first that the owner can see its own row"
1840 );
1841
1842 let admin = AclScope::user("u2", vec!["Admins".into()]).expect("scope");
1843 let ctx = Ctx::new(&storage, &snap, &admin, &options);
1844 assert_eq!(
1845 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1846 .await
1847 .expect("find")
1848 .len(),
1849 2,
1850 "a role: entry in _rperm must match a member"
1851 );
1852
1853 let stranger = AclScope::user("u3", vec![]).expect("scope");
1854 let ctx = Ctx::new(&storage, &snap, &stranger, &options);
1855 let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1856 .await
1857 .expect("find");
1858 assert_eq!(results.len(), 1);
1859 assert!(
1860 matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "public")
1861 );
1862 assert_eq!(
1863 update(
1864 &ctx,
1865 "Post",
1866 "private",
1867 body(r#"{"title":"x"}"#, OpPath::Update)
1868 )
1869 .await
1870 .unwrap_err()
1871 .code,
1872 ErrorCode::ObjectNotFound
1873 );
1874 assert_eq!(
1875 delete(&ctx, "Post", "private").await.unwrap_err().code,
1876 ErrorCode::ObjectNotFound
1877 );
1878 }
1879
1880 #[tokio::test]
1886 async fn operations_reach_storage_as_operations() {
1887 let storage = FakeStorage::new();
1888 let snap = snapshot(&storage).await;
1889 let options = opts();
1890 let master = AclScope::Unrestricted;
1891 let ctx = Ctx::new(&storage, &snap, &master, &options);
1892
1893 let created = create(
1894 &ctx,
1895 "Post",
1896 body(
1897 r#"{"views":{"__op":"Increment","amount":2},"tags":{"__op":"Add","objects":["a"]}}"#,
1898 OpPath::Create,
1899 ),
1900 )
1901 .await
1902 .expect("create");
1903
1904 let stored = storage.rows("Post");
1905 assert!(
1906 matches!(stored[0].get("views"), Some(ParseValue::Number(n)) if *n == 2.0),
1907 "an Increment must be flattened to a number, not stored as an op envelope"
1908 );
1909 assert!(matches!(stored[0].get("tags"), Some(ParseValue::Array(a)) if a.len() == 1));
1910 assert!(matches!(created.echoed.get("views"), Some(ParseValue::Number(n)) if *n == 2.0));
1911
1912 let snap = snapshot(&storage).await;
1913 let ctx = Ctx::new(&storage, &snap, &master, &options);
1914 let updated = update(
1915 &ctx,
1916 "Post",
1917 &created.object_id,
1918 body(
1919 r#"{"views":{"__op":"Increment","amount":3},"title":"plain"}"#,
1920 OpPath::Update,
1921 ),
1922 )
1923 .await
1924 .expect("update");
1925 assert!(
1926 matches!(updated.echoed.get("views"), Some(ParseValue::Number(n)) if *n == 5.0),
1927 "the response carries the post-update value"
1928 );
1929 assert!(
1930 updated.echoed.get("title").is_none(),
1931 "a plain set is not echoed"
1932 );
1933 assert!(
1934 matches!(storage.rows("Post")[0].get("views"), Some(ParseValue::Number(n)) if *n == 5.0)
1935 );
1936 }
1937
1938 #[tokio::test]
1939 async fn a_delete_op_unsets_the_field_and_echoes_nothing() {
1940 let storage = FakeStorage::new()
1941 .with_schema(default_schema("Post").with_field("title", FieldType::String))
1942 .with_row(
1943 "Post",
1944 row(vec![
1945 ("objectId", ParseValue::String("p1".into())),
1946 ("title", ParseValue::String("t".into())),
1947 ]),
1948 );
1949 let snap = snapshot(&storage).await;
1950 let options = opts();
1951 let master = AclScope::Unrestricted;
1952 let ctx = Ctx::new(&storage, &snap, &master, &options);
1953
1954 let response = update(
1955 &ctx,
1956 "Post",
1957 "p1",
1958 body(r#"{"title":{"__op":"Delete"}}"#, OpPath::Update),
1959 )
1960 .await
1961 .expect("update");
1962 assert!(response.echoed.is_empty());
1963 assert!(storage.rows("Post")[0].get("title").is_none());
1964 }
1965
1966 #[tokio::test]
1969 async fn a_type_conflict_fails_the_write_before_the_row_is_inserted() {
1970 let storage = FakeStorage::new()
1971 .with_schema(default_schema("Post").with_field("views", FieldType::Number));
1972 let snap = SchemaSnapshot::from_classes(vec![default_schema("Post")]);
1973 let options = opts();
1974 let master = AclScope::Unrestricted;
1975 let ctx = Ctx::new(&storage, &snap, &master, &options);
1976
1977 let e = create(&ctx, "Post", body(r#"{"views":"text"}"#, OpPath::Create))
1980 .await
1981 .unwrap_err();
1982 assert_eq!(e.code, ErrorCode::IncorrectType);
1983 assert_eq!(
1984 e.message,
1985 "schema mismatch for Post.views; expected Number but got String"
1986 );
1987 assert!(storage.rows("Post").is_empty(), "no row was inserted");
1988 }
1989
1990 #[tokio::test]
1991 async fn a_write_of_only_nulls_still_creates_the_class() {
1992 let storage = FakeStorage::new();
1993 let snap = snapshot(&storage).await;
1994 let options = opts();
1995 let master = AclScope::Unrestricted;
1996 let ctx = Ctx::new(&storage, &snap, &master, &options);
1997
1998 create(&ctx, "Post", body(r#"{"nothing":null}"#, OpPath::Create))
1999 .await
2000 .expect("create");
2001 let schema = storage
2002 .schema("Post")
2003 .expect("a null-only write must still leave a schema row behind");
2004 assert!(schema.field("objectId").is_some());
2005 assert!(schema.field("nothing").is_none(), "null infers no type");
2006 }
2007
2008 #[tokio::test]
2013 async fn a_relation_write_lands_in_the_join_table_and_reads_back_through_related_to() {
2014 let storage = FakeStorage::new()
2015 .with_schema(default_schema("_Role"))
2016 .with_schema(default_schema("_User"))
2017 .with_row(
2018 "_User",
2019 row(vec![("objectId", ParseValue::String("u1".into()))]),
2020 );
2021 let snap = snapshot(&storage).await;
2022 let options = opts();
2023 let master = AclScope::Unrestricted;
2024 let ctx = Ctx::new(&storage, &snap, &master, &options);
2025
2026 let created = create(
2027 &ctx,
2028 "_Role",
2029 body(
2030 r#"{"name":"admins","ACL":{"*":{"read":true}},
2031 "users":{"__op":"AddRelation","objects":[
2032 {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2033 OpPath::Create,
2034 ),
2035 )
2036 .await
2037 .expect("create");
2038
2039 let stored = storage.rows("_Role");
2040 assert!(
2041 stored[0].get("users").is_none(),
2042 "a Relation field has no column"
2043 );
2044 let joins = storage.rows("_Join:users:_Role");
2045 assert_eq!(joins.len(), 1);
2046 assert!(matches!(joins[0].get("relatedId"), Some(ParseValue::String(id)) if id == "u1"));
2047 assert!(
2048 matches!(joins[0].get("owningId"), Some(ParseValue::String(id)) if *id == created.object_id)
2049 );
2050 assert!(
2051 storage.schema("_Join:users:_Role").is_none(),
2052 "a join collection has no _SCHEMA row"
2053 );
2054
2055 let snap = snapshot(&storage).await;
2057 let ctx = Ctx::new(&storage, &snap, &master, &options);
2058 update(
2059 &ctx,
2060 "_Role",
2061 &created.object_id,
2062 body(
2063 r#"{"users":{"__op":"AddRelation","objects":[
2064 {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2065 OpPath::Update,
2066 ),
2067 )
2068 .await
2069 .expect("update");
2070 assert_eq!(storage.rows("_Join:users:_Role").len(), 1);
2071
2072 let query = format!(
2074 r#"{{"$relatedTo":{{"object":{{"__type":"Pointer","className":"_Role","objectId":"{}"}},"key":"users"}}}}"#,
2075 created.object_id
2076 );
2077 let members = find(&ctx, "_User", where_(&query), FindOptions::default())
2078 .await
2079 .expect("find");
2080 assert_eq!(members.len(), 1);
2081
2082 update(
2084 &ctx,
2085 "_Role",
2086 &created.object_id,
2087 body(
2088 r#"{"users":{"__op":"RemoveRelation","objects":[
2089 {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2090 OpPath::Update,
2091 ),
2092 )
2093 .await
2094 .expect("update");
2095 assert!(storage.rows("_Join:users:_Role").is_empty());
2096 }
2097
2098 #[tokio::test]
2101 async fn a_related_to_the_caller_cannot_read_yields_empty_rather_than_an_error() {
2102 let storage = FakeStorage::new()
2103 .with_schema(default_schema("_Role"))
2104 .with_schema(default_schema("_User"))
2105 .with_row(
2106 "_Role",
2107 row(vec![
2108 ("objectId", ParseValue::String("r1".into())),
2109 ("_rperm", strings(&["u2"])),
2110 ]),
2111 )
2112 .with_row(
2113 "_User",
2114 row(vec![("objectId", ParseValue::String("u1".into()))]),
2115 )
2116 .with_row(
2117 "_Join:users:_Role",
2118 row(vec![
2119 ("relatedId", ParseValue::String("u1".into())),
2120 ("owningId", ParseValue::String("r1".into())),
2121 ]),
2122 );
2123 let snap = snapshot(&storage).await;
2124 let options = opts();
2125 let query = r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#;
2126
2127 let outsider = AclScope::user("u3", vec![]).expect("scope");
2128 let ctx = Ctx::new(&storage, &snap, &outsider, &options);
2129 let results = find(&ctx, "_User", where_(query), FindOptions::default())
2130 .await
2131 .expect("a denied relation is empty, not an error");
2132 assert!(results.is_empty());
2133
2134 let insider = AclScope::user("u2", vec![]).expect("scope");
2137 let ctx = Ctx::new(&storage, &snap, &insider, &options);
2138 assert_eq!(
2139 find(&ctx, "_User", where_(query), FindOptions::default())
2140 .await
2141 .expect("find")
2142 .len(),
2143 1
2144 );
2145 }
2146
2147 #[tokio::test]
2150 async fn a_related_to_on_a_protected_key_is_forbidden() {
2151 let storage = FakeStorage::new()
2152 .with_schema(default_schema("_User"))
2153 .with_schema(
2154 default_schema("_Role")
2155 .with_field(
2156 "users",
2157 FieldType::Relation {
2158 target_class: "_User".into(),
2159 },
2160 )
2161 .with_clp(clp(r#"{"protectedFields":{"*":["users"]}}"#)),
2162 )
2163 .with_row(
2164 "_Role",
2165 row(vec![("objectId", ParseValue::String("r1".into()))]),
2166 );
2167 let snap = snapshot(&storage).await;
2168 let options = opts();
2169 let user = AclScope::user("u1", vec![]).expect("scope");
2170 let ctx = Ctx::new(&storage, &snap, &user, &options);
2171 let query = r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#;
2172 let e = find(&ctx, "_User", where_(query), FindOptions::default())
2173 .await
2174 .unwrap_err();
2175 assert_eq!(e.code, ErrorCode::OperationForbidden);
2176 assert_eq!(e.message, "Permission denied");
2177
2178 let disclosing = disclosing_opts();
2179 let disclosed = Ctx::new(&storage, &snap, &user, &disclosing);
2180 assert_eq!(
2181 find(&disclosed, "_User", where_(query), FindOptions::default())
2182 .await
2183 .unwrap_err()
2184 .message,
2185 "This user is not allowed to query users on class _Role"
2186 );
2187 }
2188
2189 #[tokio::test]
2190 async fn a_constraint_on_a_relation_field_is_the_reverse_join() {
2191 let storage = FakeStorage::new()
2192 .with_schema(default_schema("_User"))
2193 .with_schema(default_schema("_Role").with_field(
2194 "users",
2195 FieldType::Relation {
2196 target_class: "_User".into(),
2197 },
2198 ))
2199 .with_row(
2200 "_Role",
2201 row(vec![("objectId", ParseValue::String("r1".into()))]),
2202 )
2203 .with_row(
2204 "_Role",
2205 row(vec![("objectId", ParseValue::String("r2".into()))]),
2206 )
2207 .with_row(
2208 "_Join:users:_Role",
2209 row(vec![
2210 ("relatedId", ParseValue::String("u1".into())),
2211 ("owningId", ParseValue::String("r1".into())),
2212 ]),
2213 );
2214 let snap = snapshot(&storage).await;
2215 let options = opts();
2216 let master = AclScope::Unrestricted;
2217 let ctx = Ctx::new(&storage, &snap, &master, &options);
2218
2219 let results = find(
2220 &ctx,
2221 "_Role",
2222 where_(r#"{"users":{"__type":"Pointer","className":"_User","objectId":"u1"}}"#),
2223 FindOptions::default(),
2224 )
2225 .await
2226 .expect("find");
2227 assert_eq!(results.len(), 1);
2228 assert!(matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "r1"));
2229 }
2230
2231 #[tokio::test]
2238 async fn include_applies_the_target_class_acl() {
2239 let storage = FakeStorage::new()
2240 .with_schema(default_schema("_User").with_field("nickname", FieldType::String))
2241 .with_schema(default_schema("Post").with_field(
2242 "author",
2243 FieldType::Pointer {
2244 target_class: "_User".into(),
2245 },
2246 ))
2247 .with_row(
2248 "Post",
2249 row(vec![
2250 ("objectId", ParseValue::String("p1".into())),
2251 ("author", pointer("_User", "u1")),
2252 ]),
2253 )
2254 .with_row(
2255 "_User",
2256 row(vec![
2257 ("objectId", ParseValue::String("u1".into())),
2258 ("nickname", ParseValue::String("nick".into())),
2259 ("_hashed_password", ParseValue::String("hash".into())),
2260 ("sessionToken", ParseValue::String("r:t".into())),
2261 ("_rperm", strings(&["u1"])),
2262 ]),
2263 );
2264 let snap = snapshot(&storage).await;
2265 let options = opts();
2266 let include = FindOptions {
2267 include: vec![vec!["author".to_string()]],
2268 ..Default::default()
2269 };
2270
2271 let anon = AclScope::Anonymous;
2272 let ctx = Ctx::new(&storage, &snap, &anon, &options);
2273 let results = find(&ctx, "Post", ParsedWhere::default(), include.clone())
2274 .await
2275 .expect("find");
2276 assert_eq!(results.len(), 1);
2277 assert!(
2278 results[0].get("author").is_none(),
2279 "an unreadable pointer is dropped rather than expanded or left as a pointer"
2280 );
2281
2282 let owner = AclScope::user("u1", vec![]).expect("scope");
2283 let ctx = Ctx::new(&storage, &snap, &owner, &options);
2284 let results = find(&ctx, "Post", ParsedWhere::default(), include)
2285 .await
2286 .expect("find");
2287 match results[0].get("author") {
2288 Some(ParseValue::Object(author)) => {
2289 assert!(
2290 matches!(author.get("nickname"), Some(ParseValue::String(n)) if n == "nick")
2291 );
2292 assert!(author.get("_hashed_password").is_none());
2293 assert!(author.get("sessionToken").is_none());
2294 assert!(
2295 matches!(author.get("__type"), Some(ParseValue::String(t)) if t == "Object")
2296 );
2297 }
2298 other => panic!("expected an expanded author, got {other:?}"),
2299 }
2300 }
2301
2302 #[tokio::test]
2303 async fn a_dotted_include_resolves_parents_before_children() {
2304 let storage = FakeStorage::new()
2305 .with_schema(default_schema("Company").with_field("name", FieldType::String))
2306 .with_schema(default_schema("_User").with_field(
2307 "company",
2308 FieldType::Pointer {
2309 target_class: "Company".into(),
2310 },
2311 ))
2312 .with_schema(default_schema("Post").with_field(
2313 "author",
2314 FieldType::Pointer {
2315 target_class: "_User".into(),
2316 },
2317 ))
2318 .with_row(
2319 "Post",
2320 row(vec![
2321 ("objectId", ParseValue::String("p1".into())),
2322 ("author", pointer("_User", "u1")),
2323 ]),
2324 )
2325 .with_row(
2326 "_User",
2327 row(vec![
2328 ("objectId", ParseValue::String("u1".into())),
2329 ("company", pointer("Company", "c1")),
2330 ]),
2331 )
2332 .with_row(
2333 "Company",
2334 row(vec![
2335 ("objectId", ParseValue::String("c1".into())),
2336 ("name", ParseValue::String("Acme".into())),
2337 ]),
2338 );
2339 let snap = snapshot(&storage).await;
2340 let options = opts();
2341 let anon = AclScope::Anonymous;
2342 let ctx = Ctx::new(&storage, &snap, &anon, &options);
2343
2344 let results = find(
2345 &ctx,
2346 "Post",
2347 ParsedWhere::default(),
2348 FindOptions {
2349 include: crate::query_parse::parse_include("author.company").expect("include"),
2350 ..Default::default()
2351 },
2352 )
2353 .await
2354 .expect("find");
2355
2356 let Some(ParseValue::Object(author)) = results[0].get("author") else {
2357 panic!("author should be expanded");
2358 };
2359 let Some(ParseValue::Object(company)) = author.get("company") else {
2360 panic!("company should be expanded");
2361 };
2362 assert!(matches!(company.get("name"), Some(ParseValue::String(n)) if n == "Acme"));
2363 }
2364
2365 #[tokio::test]
2369 async fn a_client_cannot_bring_a_class_into_existence_at_the_default() {
2370 let storage = FakeStorage::new();
2371 let snap = snapshot(&storage).await;
2372 let options = opts();
2373 let anon = AclScope::Anonymous;
2374 let ctx = Ctx::new(&storage, &snap, &anon, &options);
2375
2376 let err = create(&ctx, "BrandNew", body(r#"{"x":1}"#, OpPath::Create))
2377 .await
2378 .expect_err("a client must not create a class at the default");
2379 assert_eq!(err.code, ErrorCode::OperationForbidden);
2380 assert!(
2381 storage.schema("BrandNew").is_none(),
2382 "the refusal must leave no _SCHEMA row behind"
2383 );
2384 }
2385
2386 #[tokio::test]
2389 async fn master_the_option_and_the_system_classes_are_all_exempt() {
2390 for (label, scope, allow, class) in [
2391 ("option on", AclScope::Anonymous, true, "BrandNew"),
2392 ("master", AclScope::Unrestricted, false, "BrandNew"),
2393 ("system class", AclScope::Anonymous, false, "_User"),
2394 ] {
2395 let storage = FakeStorage::new();
2396 let snap = snapshot(&storage).await;
2397 let options = PermissionOptions {
2398 allow_client_class_creation: allow,
2399 ..PermissionOptions::default()
2400 };
2401 let ctx = Ctx::new(&storage, &snap, &scope, &options);
2402 create(&ctx, class, body(r#"{"x":1}"#, OpPath::Create))
2403 .await
2404 .unwrap_or_else(|e| panic!("{label} should be allowed to create {class}: {e:?}"));
2405 }
2406 }
2407}
2408
2409#[cfg(test)]
2410mod relation_schema_tests {
2411 use super::tests_support::*;
2412 use super::*;
2413 use crate::testing::FakeStorage;
2414 use parse_rust_core::op::OpPath;
2415
2416 #[tokio::test]
2420 async fn a_relation_op_reserves_the_field_type_before_it_is_stripped() {
2421 let storage = FakeStorage::new()
2422 .with_schema(default_schema("_User"))
2423 .with_row("_User", single("objectId", ParseValue::String("u1".into())));
2424 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2425 let options = PermissionOptions::default();
2426 let master = AclScope::Unrestricted;
2427 let ctx = Ctx::new(&storage, &snap, &master, &options);
2428
2429 let created = create(
2430 &ctx,
2431 "Team",
2432 decode(
2433 r#"{"name":"core","members":{"__op":"AddRelation","objects":[
2434 {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2435 OpPath::Create,
2436 ),
2437 )
2438 .await
2439 .expect("create");
2440
2441 let schema = storage.schema("Team").expect("class reserved");
2442 assert_eq!(
2443 schema.field("members"),
2444 Some(&FieldType::Relation {
2445 target_class: "_User".into()
2446 }),
2447 "without this a $relatedTo against Team.members resolves to nothing forever"
2448 );
2449 assert!(storage.rows("Team")[0].get("members").is_none());
2450 assert_eq!(storage.rows("_Join:members:Team").len(), 1);
2451
2452 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2454 let ctx = Ctx::new(&storage, &snap, &master, &options);
2455 let query = format!(
2456 r#"{{"$relatedTo":{{"object":{{"__type":"Pointer","className":"Team","objectId":"{}"}},"key":"members"}}}}"#,
2457 created.object_id
2458 );
2459 assert_eq!(
2460 find(
2461 &ctx,
2462 "_User",
2463 parse_json_where(&query),
2464 FindOptions::default()
2465 )
2466 .await
2467 .expect("find")
2468 .len(),
2469 1
2470 );
2471 }
2472}
2473
2474#[cfg(test)]
2475mod tests_support {
2476 use super::*;
2477 use crate::query_parse::parse_where;
2478 use crate::write::decode_write_body;
2479 use parse_rust_core::op::OpPath;
2480
2481 pub fn single(key: &str, value: ParseValue) -> ParseMap {
2482 let mut m = ParseMap::new();
2483 m.insert(key.to_string(), value);
2484 m
2485 }
2486
2487 pub fn decode(json: &str, path: OpPath) -> WriteBody {
2488 decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
2489 }
2490
2491 pub fn parse_json_where(json: &str) -> ParsedWhere {
2492 parse_where(&serde_json::from_str(json).expect("test literal")).expect("parse")
2493 }
2494}
2495
2496#[cfg(test)]
2497mod write_edge_tests {
2498 use super::tests_support::*;
2499 use super::*;
2500 use crate::testing::FakeStorage;
2501 use parse_rust_core::op::OpPath;
2502
2503 #[tokio::test]
2512 async fn a_falsy_acl_on_an_update_does_not_clear_the_permissions() {
2513 for body in [
2514 r#"{"ACL":null,"title":"t"}"#,
2515 r#"{"ACL":false,"title":"t"}"#,
2516 r#"{"ACL":0,"title":"t"}"#,
2517 r#"{"ACL":"","title":"t"}"#,
2518 ] {
2519 assert_falsy_acl_preserves_permissions(body).await;
2520 }
2521 }
2522
2523 async fn assert_falsy_acl_preserves_permissions(body: &str) {
2524 let mut existing = single("objectId", ParseValue::String("p1".into()));
2525 existing.insert(
2526 "_rperm".to_string(),
2527 ParseValue::Array(vec![ParseValue::String("u1".into())]),
2528 );
2529 existing.insert(
2530 "_wperm".to_string(),
2531 ParseValue::Array(vec![ParseValue::String("u1".into())]),
2532 );
2533 let storage = FakeStorage::new()
2534 .with_schema(default_schema("Post").with_field("title", FieldType::String))
2535 .with_row("Post", existing);
2536 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2537 let options = PermissionOptions::default();
2538 let master = AclScope::Unrestricted;
2539 let ctx = Ctx::new(&storage, &snap, &master, &options);
2540
2541 update(&ctx, "Post", "p1", decode(body, OpPath::Update))
2542 .await
2543 .expect("update");
2544
2545 let stored = &storage.rows("Post")[0];
2546 assert!(
2547 matches!(stored.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 1),
2548 "the existing permissions must survive a falsy ACL: {body}"
2549 );
2550 }
2551
2552 #[tokio::test]
2562 async fn an_update_to_a_missing_class_creates_the_class_and_then_finds_nothing() {
2563 for body in [r#"{}"#, r#"{"title":"a"}"#, r#"{"bad-key":1}"#] {
2568 let storage = FakeStorage::new();
2569 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2570 let options = PermissionOptions::default();
2571 let master = AclScope::Unrestricted;
2572 let ctx = Ctx::new(&storage, &snap, &master, &options);
2573
2574 let e = update(&ctx, "Ghost", "p1", decode(body, OpPath::Update))
2575 .await
2576 .unwrap_err();
2577 let expected = if body.contains("bad-key") {
2578 ErrorCode::InvalidKeyName
2579 } else {
2580 ErrorCode::ObjectNotFound
2581 };
2582 assert_eq!(e.code, expected, "for body {body}");
2583 assert!(
2584 storage.schema("Ghost").is_some(),
2585 "the schema row survives the failed update, for body {body}"
2586 );
2587 }
2588 }
2589
2590 #[tokio::test]
2592 async fn an_acl_on_an_update_replaces_both_columns() {
2593 let storage = FakeStorage::new()
2594 .with_schema(default_schema("Post"))
2595 .with_row("Post", single("objectId", ParseValue::String("p1".into())));
2596 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2597 let options = PermissionOptions::default();
2598 let master = AclScope::Unrestricted;
2599 let ctx = Ctx::new(&storage, &snap, &master, &options);
2600
2601 update(
2602 &ctx,
2603 "Post",
2604 "p1",
2605 decode(
2606 r#"{"ACL":{"u1":{"read":true,"write":true},"*":{"read":true}}}"#,
2607 OpPath::Update,
2608 ),
2609 )
2610 .await
2611 .expect("update");
2612
2613 let stored = &storage.rows("Post")[0];
2614 assert!(stored.get("ACL").is_none(), "ACL is not a stored column");
2615 assert!(matches!(stored.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 2));
2616 assert!(matches!(stored.get("_wperm"), Some(ParseValue::Array(a)) if a.len() == 1));
2617 }
2618}