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::{default_acl_for_create, 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 mut generated_acl = None;
987 if let Some(declared) = clp.and_then(|c| c.default_acl()) {
988 let suppressed = match body.get("ACL") {
989 Some(FieldWrite::Value(v)) => parse_rust_core::is_js_truthy(v),
990 Some(FieldWrite::Op(_)) => true,
991 None => false,
992 };
993 if !suppressed {
994 let acl = default_acl_for_create(declared, ctx.scope.user_id());
995 generated_acl = Some(acl.clone());
996 body.insert("ACL".to_string(), FieldWrite::Value(acl));
997 }
998 }
999
1000 let object_id = match body.get("objectId") {
1015 None => new_object_id(),
1016 Some(FieldWrite::Value(v)) if !parse_rust_core::is_js_truthy(v) => new_object_id(),
1017 Some(FieldWrite::Value(ParseValue::String(id))) => id.clone(),
1018 Some(other) => {
1019 let got = match other {
1020 FieldWrite::Value(v) => infer_type(v),
1021 FieldWrite::Op(op) => infer_op_type(op)?,
1022 };
1023 return Err(match got {
1027 Some(got) => schema_mismatch(class_name, "objectId", &FieldType::String, &got),
1028 None => ParseError::invalid_json("objectId is an invalid field name."),
1037 });
1038 }
1039 };
1040 let now = ParseDate::now();
1041 body.insert(
1042 "objectId".to_string(),
1043 FieldWrite::Value(ParseValue::String(object_id.clone())),
1044 );
1045 body.insert(
1046 "createdAt".to_string(),
1047 FieldWrite::Value(ParseValue::Date(now)),
1048 );
1049 body.insert(
1050 "updatedAt".to_string(),
1051 FieldWrite::Value(ParseValue::Date(now)),
1052 );
1053
1054 let delta = validate_write_fields(&schema, &body)?;
1061 reserve_schema(ctx, class_name, &schema, &delta.added).await?;
1062 apply(&mut schema, &delta);
1063
1064 let relation_updates = relations::collect_relation_updates(&mut body);
1065
1066 if matches!(body.get("ACL"), Some(FieldWrite::Op(_))) {
1081 body.insert(
1082 "ACL".to_string(),
1083 FieldWrite::Value(ParseValue::Object(ParseMap::new())),
1084 );
1085 }
1086
1087 let row = lower_acl(flatten_for_create(&body)?);
1090 ctx.storage.create(&schema, &row).await?;
1091
1092 relations::apply_relation_updates(ctx.storage, class_name, &object_id, &relation_updates)
1093 .await?;
1094
1095 let mut echoed = echo_response(&body, Some(&row));
1099 if let Some(acl) = generated_acl {
1100 echoed.insert("ACL".to_string(), acl);
1101 }
1102
1103 Ok(CreateResponse {
1104 object_id,
1105 created_at: now,
1106 echoed,
1107 })
1108}
1109
1110pub async fn update<S: StorageAdapter>(
1112 ctx: &Ctx<'_, S>,
1113 class_name: &str,
1114 object_id: &str,
1115 mut body: WriteBody,
1116) -> Result<UpdateResponse, ParseError> {
1117 let class_exists = ctx.snapshot.contains(class_name);
1118 let mut schema = ctx.snapshot.resolve_for_write(class_name);
1119 let clp = ctx.snapshot.clp(class_name);
1120 let acl_group = ctx.scope.acl_group();
1121 let master = ctx.scope.is_master();
1122
1123 body.shift_remove("objectId");
1125 body.shift_remove("createdAt");
1126
1127 validate_required_columns(class_name, &as_plain_body(&body), true)?;
1128
1129 let introduces_field = adds_field(
1130 &schema,
1131 class_exists,
1132 body.keys().map(String::as_str),
1133 |key| matches!(body.get(key), Some(FieldWrite::Op(Op::Delete))),
1134 );
1135 if !master && introduces_field {
1136 validate_permission(
1137 clp,
1138 class_name,
1139 &acl_group,
1140 Operation::AddField,
1141 Some(WriteAction::Update),
1142 ctx.options.error_detail,
1143 )?;
1144 }
1145
1146 if !master {
1147 validate_permission(
1148 clp,
1149 class_name,
1150 &acl_group,
1151 Operation::Update,
1152 Some(WriteAction::Update),
1153 ctx.options.error_detail,
1154 )?;
1155 }
1156
1157 let mut query = Query::from_constraints(vec![Constraint::equal(
1158 "objectId",
1159 ParseValue::String(object_id.to_string()),
1160 )]);
1161 if !master {
1162 match apply_pointer_permissions(&schema, clp, Operation::Update, &acl_group, &query)? {
1163 PointerPermOutcome::Unconstrained => {}
1164 PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1165 PointerPermOutcome::DenyAll => return Err(object_not_found()),
1169 }
1170 if introduces_field {
1171 match apply_pointer_permissions(&schema, clp, Operation::AddField, &acl_group, &query)?
1174 {
1175 PointerPermOutcome::Unconstrained => {}
1176 PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1177 PointerPermOutcome::DenyAll => return Err(object_not_found()),
1178 }
1179 }
1180 if let Some(constraint) = ctx.scope.write_constraint() {
1181 query.push_constraint(constraint);
1182 }
1183 }
1184
1185 let updated_at = ParseDate::now();
1186 body.insert(
1187 "updatedAt".to_string(),
1188 FieldWrite::Value(ParseValue::Date(updated_at)),
1189 );
1190
1191 ensure_class_exists(ctx, class_name, class_exists).await?;
1209 let delta = validate_write_fields(&schema, &body)?;
1210 reserve_schema(ctx, class_name, &schema, &delta.added).await?;
1211 apply(&mut schema, &delta);
1212
1213 let relation_updates = relations::collect_relation_updates(&mut body);
1214
1215 let mut update = lower_update(&body)?;
1216 lower_acl_into_update(&mut body, &mut update);
1217
1218 let echoed = if echoed_keys(&body).is_empty() {
1220 let matched = ctx.storage.update(&schema, &query, &update).await?;
1221 if matched == 0 {
1222 return Err(object_not_found());
1223 }
1224 ParseMap::new()
1225 } else {
1226 let row = ctx
1227 .storage
1228 .update_one_returning(&schema, &query, &update)
1229 .await?;
1230 let Some(row) = row else {
1231 return Err(object_not_found());
1232 };
1233 echo_response(&body, Some(&row))
1234 };
1235
1236 relations::apply_relation_updates(ctx.storage, class_name, object_id, &relation_updates)
1237 .await?;
1238
1239 Ok(UpdateResponse { updated_at, echoed })
1240}
1241
1242fn lower_acl_into_update(body: &mut WriteBody, update: &mut parse_rust_storage::Update) {
1248 let Some(write) = body.shift_remove("ACL") else {
1249 return;
1250 };
1251 update.shift_remove("ACL");
1252 let value = match write {
1253 FieldWrite::Value(v) if !parse_rust_core::is_js_truthy(&v) => return,
1264 FieldWrite::Value(value) => value,
1265 FieldWrite::Op(_) => ParseValue::Object(ParseMap::new()),
1268 };
1269 let mut carrier = ParseMap::new();
1270 carrier.insert("ACL".to_string(), value);
1271 let lowered = lower_acl(carrier);
1272 for key in ["_rperm", "_wperm"] {
1273 let value = lowered
1274 .get(key)
1275 .cloned()
1276 .unwrap_or(ParseValue::Array(Vec::new()));
1277 update.insert(key.to_string(), UpdateValue::Set(value));
1278 }
1279}
1280
1281pub async fn delete<S: StorageAdapter>(
1283 ctx: &Ctx<'_, S>,
1284 class_name: &str,
1285 object_id: &str,
1286) -> Result<(), ParseError> {
1287 let schema = ctx.snapshot.get_or_default(class_name);
1288 let clp = ctx.snapshot.clp(class_name);
1289 let acl_group = ctx.scope.acl_group();
1290 let master = ctx.scope.is_master();
1291
1292 if !master {
1293 validate_permission(
1294 clp,
1295 class_name,
1296 &acl_group,
1297 Operation::Delete,
1298 None,
1299 ctx.options.error_detail,
1300 )?;
1301 }
1302
1303 let mut query = Query::from_constraints(vec![Constraint::equal(
1304 "objectId",
1305 ParseValue::String(object_id.to_string()),
1306 )]);
1307 if !master {
1308 match apply_pointer_permissions(&schema, clp, Operation::Delete, &acl_group, &query)? {
1309 PointerPermOutcome::Unconstrained => {}
1310 PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1311 PointerPermOutcome::DenyAll => return Err(object_not_found()),
1313 }
1314 if let Some(constraint) = ctx.scope.write_constraint() {
1315 query.push_constraint(constraint);
1316 }
1317 }
1318
1319 let deleted = ctx.storage.delete(&schema, &query).await?;
1320 if deleted == 0 {
1321 return Err(object_not_found());
1322 }
1323 Ok(())
1324}
1325
1326async fn ensure_class_exists<S: StorageAdapter>(
1360 ctx: &Ctx<'_, S>,
1361 class_name: &str,
1362 class_exists: bool,
1363) -> Result<(), ParseError> {
1364 if class_exists {
1365 return Ok(());
1366 }
1367 validate_client_class_creation(ctx, class_name, true)?;
1368 if !parse_rust_schema::class_name_is_valid(class_name) {
1369 return Err(ParseError::invalid_json(
1370 "schema class name does not revalidate",
1371 ));
1372 }
1373 ctx.storage.upsert_schema(&default_schema(class_name)).await
1374}
1375
1376fn validate_client_class_creation<S: StorageAdapter>(
1392 ctx: &Ctx<'_, S>,
1393 class_name: &str,
1394 maintenance_is_exempt: bool,
1395) -> Result<(), ParseError> {
1396 let privileged = if maintenance_is_exempt {
1402 ctx.scope.is_master()
1403 } else {
1404 ctx.scope.is_master() && !ctx.is_maintenance
1405 };
1406 if ctx.options.allow_client_class_creation
1407 || privileged
1408 || parse_rust_schema::SYSTEM_CLASSES.contains(&class_name)
1409 {
1410 return Ok(());
1411 }
1412 Err(ParseError::permission_denied(
1415 ErrorCode::OperationForbidden,
1416 format!("This user is not allowed to access non-existent class: {class_name}"),
1417 ctx.options.error_detail,
1418 ))
1419}
1420
1421async fn reserve_schema<S: StorageAdapter>(
1422 ctx: &Ctx<'_, S>,
1423 class_name: &str,
1424 schema: &ClassSchema,
1425 added: &[(String, FieldType)],
1426) -> Result<(), ParseError> {
1427 for (field_name, field_type) in added {
1428 match ctx
1429 .storage
1430 .reserve_field(class_name, field_name, field_type, None)
1433 .await?
1434 {
1435 AddFieldOutcome::Added | AddFieldOutcome::AlreadyPresentSameType => {}
1436 AddFieldOutcome::Conflict { existing } => {
1437 return Err(schema_mismatch(
1441 &schema.class_name,
1442 field_name,
1443 &existing,
1444 field_type,
1445 ));
1446 }
1447 }
1448 }
1449 Ok(())
1450}
1451
1452#[cfg(test)]
1453mod tests {
1454 use super::*;
1455 use crate::query_parse::parse_where;
1456 use crate::testing::FakeStorage;
1457 use crate::write::decode_write_body;
1458 use parse_rust_core::op::OpPath;
1459 use parse_rust_core::ClassLevelPermissions;
1460
1461 fn clp(json: &str) -> ClassLevelPermissions {
1462 let value = parse_rust_core::classify(
1463 serde_json::from_str(json).expect("test literal must be valid JSON"),
1464 )
1465 .expect("classify");
1466 match value {
1467 ParseValue::Object(m) => ClassLevelPermissions::from_map(m),
1468 _ => panic!("expected an object"),
1469 }
1470 }
1471
1472 fn where_(json: &str) -> ParsedWhere {
1473 parse_where(&serde_json::from_str(json).expect("test literal")).expect("parse")
1474 }
1475
1476 fn body(json: &str, path: OpPath) -> WriteBody {
1477 decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
1478 }
1479
1480 fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
1481 let mut m = ParseMap::new();
1482 for (k, v) in pairs {
1483 m.insert(k.to_string(), v);
1484 }
1485 m
1486 }
1487
1488 fn strings(values: &[&str]) -> ParseValue {
1489 ParseValue::Array(
1490 values
1491 .iter()
1492 .map(|v| ParseValue::String((*v).to_string()))
1493 .collect(),
1494 )
1495 }
1496
1497 fn pointer(class: &str, id: &str) -> ParseValue {
1498 ParseValue::Pointer {
1499 class_name: class.to_string(),
1500 object_id: id.to_string(),
1501 }
1502 }
1503
1504 async fn snapshot(storage: &FakeStorage) -> SchemaSnapshot {
1505 SchemaSnapshot::load(storage).await.expect("snapshot")
1506 }
1507
1508 fn opts() -> PermissionOptions {
1511 PermissionOptions::default()
1512 }
1513
1514 fn disclosing_opts() -> PermissionOptions {
1517 PermissionOptions {
1518 error_detail: parse_rust_core::ErrorDetail::Disclosed,
1519 ..PermissionOptions::default()
1520 }
1521 }
1522
1523 #[tokio::test]
1529 async fn create_runs_the_clp_gate() {
1530 let storage = FakeStorage::new().with_schema(
1531 default_schema("Post").with_clp(clp(r#"{"create":{"role:Writers":true}}"#)),
1532 );
1533 let snap = snapshot(&storage).await;
1534 let options = opts();
1535
1536 let anon = AclScope::Anonymous;
1537 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1538 let e = create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1539 .await
1540 .unwrap_err();
1541 assert_eq!(e.code, ErrorCode::OperationForbidden);
1542 assert_eq!(e.message, "Permission denied");
1543 assert!(storage.rows("Post").is_empty(), "nothing was written");
1544
1545 let disclosing = disclosing_opts();
1546 let ctx = Ctx::new(&storage, &snap, &anon, &disclosing);
1547 assert_eq!(
1548 create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1549 .await
1550 .unwrap_err()
1551 .message,
1552 "Permission denied for action create on class Post."
1553 );
1554 assert!(storage.rows("Post").is_empty(), "nothing was written");
1555
1556 let writer = AclScope::user("u1", vec!["Writers".into()]).expect("scope");
1557 let ctx = Ctx::new(&storage, &snap, &writer, &options);
1558 assert!(
1559 create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1560 .await
1561 .is_ok()
1562 );
1563 }
1564
1565 #[tokio::test]
1567 async fn a_class_with_no_clp_is_unrestricted() {
1568 let storage = FakeStorage::new().with_schema(default_schema("Post"));
1569 let snap = snapshot(&storage).await;
1570 let options = opts();
1571 let anon = AclScope::Anonymous;
1572 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1573 assert!(
1574 create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1575 .await
1576 .is_ok()
1577 );
1578 assert!(
1579 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1580 .await
1581 .is_ok()
1582 );
1583 }
1584
1585 #[tokio::test]
1587 async fn requires_authentication_denies_a_read_with_object_not_found() {
1588 let storage = FakeStorage::new()
1589 .with_schema(
1590 default_schema("Post").with_clp(clp(r#"{"find":{"requiresAuthentication":true}}"#)),
1591 )
1592 .with_row(
1593 "Post",
1594 row(vec![("objectId", ParseValue::String("p1".into()))]),
1595 );
1596 let snap = snapshot(&storage).await;
1597 let options = opts();
1598
1599 let anon = AclScope::Anonymous;
1600 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1601 let e = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1602 .await
1603 .unwrap_err();
1604 assert_eq!(e.code, ErrorCode::ObjectNotFound);
1605 assert_eq!(e.message, "Permission denied");
1606
1607 let disclosing = disclosing_opts();
1608 let disclosed = Ctx::new(&storage, &snap, &anon, &disclosing);
1609 let e = find(
1610 &disclosed,
1611 "Post",
1612 ParsedWhere::default(),
1613 FindOptions::default(),
1614 )
1615 .await
1616 .unwrap_err();
1617 assert_eq!(e.code, ErrorCode::ObjectNotFound);
1618 assert_eq!(
1619 e.message,
1620 "Permission denied, user needs to be authenticated."
1621 );
1622
1623 let user = AclScope::user("u1", vec![]).expect("scope");
1624 let ctx = Ctx::new(&storage, &snap, &user, &options);
1625 assert_eq!(
1626 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1627 .await
1628 .expect("allowed")
1629 .len(),
1630 1
1631 );
1632 }
1633
1634 fn pointer_perm_storage() -> FakeStorage {
1639 let clp_json = r#"{
1640 "find":{"pointerFields":["owner"]},
1641 "get":{"pointerFields":["owner"]},
1642 "count":{"pointerFields":["owner"]},
1643 "update":{"pointerFields":["owner"]},
1644 "delete":{"pointerFields":["owner"]},
1645 "create":{"*":true}
1646 }"#;
1647 FakeStorage::new()
1648 .with_schema(
1649 default_schema("Post")
1650 .with_field(
1651 "owner",
1652 FieldType::Pointer {
1653 target_class: "_User".into(),
1654 },
1655 )
1656 .with_field("title", FieldType::String)
1657 .with_clp(clp(clp_json)),
1658 )
1659 .with_row(
1660 "Post",
1661 row(vec![
1662 ("objectId", ParseValue::String("p1".into())),
1663 ("owner", pointer("_User", "u1")),
1664 ("title", ParseValue::String("mine".into())),
1665 ]),
1666 )
1667 .with_row(
1668 "Post",
1669 row(vec![
1670 ("objectId", ParseValue::String("p2".into())),
1671 ("owner", pointer("_User", "u2")),
1672 ("title", ParseValue::String("theirs".into())),
1673 ]),
1674 )
1675 }
1676
1677 #[tokio::test]
1681 async fn an_anonymous_caller_gets_nothing_from_a_pointer_permission_class() {
1682 let storage = pointer_perm_storage();
1683 let snap = snapshot(&storage).await;
1684 let options = opts();
1685 let anon = AclScope::Anonymous;
1686 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1687
1688 assert!(
1689 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1690 .await
1691 .expect("find resolves empty rather than erroring")
1692 .is_empty()
1693 );
1694 assert_eq!(
1695 get(&ctx, "Post", "p1", FindOptions::default())
1696 .await
1697 .unwrap_err()
1698 .code,
1699 ErrorCode::ObjectNotFound
1700 );
1701 assert_eq!(
1702 count(&ctx, "Post", ParsedWhere::default())
1703 .await
1704 .expect("count resolves"),
1705 0
1706 );
1707 assert_eq!(
1708 update(&ctx, "Post", "p1", body(r#"{"title":"x"}"#, OpPath::Update))
1709 .await
1710 .unwrap_err()
1711 .code,
1712 ErrorCode::ObjectNotFound
1713 );
1714 assert_eq!(
1715 delete(&ctx, "Post", "p1").await.unwrap_err().code,
1716 ErrorCode::ObjectNotFound
1717 );
1718 assert_eq!(storage.rows("Post").len(), 2, "nothing was deleted");
1719 }
1720
1721 #[tokio::test]
1722 async fn a_pointer_permission_narrows_a_user_to_their_own_rows() {
1723 let storage = pointer_perm_storage();
1724 let snap = snapshot(&storage).await;
1725 let options = opts();
1726 let u1 = AclScope::user("u1", vec![]).expect("scope");
1727 let ctx = Ctx::new(&storage, &snap, &u1, &options);
1728
1729 let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1730 .await
1731 .expect("find");
1732 assert_eq!(results.len(), 1);
1733 assert!(matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "p1"));
1734
1735 assert!(get(&ctx, "Post", "p1", FindOptions::default())
1736 .await
1737 .is_ok());
1738 assert_eq!(
1739 get(&ctx, "Post", "p2", FindOptions::default())
1740 .await
1741 .unwrap_err()
1742 .code,
1743 ErrorCode::ObjectNotFound
1744 );
1745 assert_eq!(
1746 count(&ctx, "Post", ParsedWhere::default())
1747 .await
1748 .expect("count"),
1749 1
1750 );
1751 assert!(
1752 update(&ctx, "Post", "p1", body(r#"{"title":"x"}"#, OpPath::Update))
1753 .await
1754 .is_ok()
1755 );
1756 assert_eq!(
1757 update(&ctx, "Post", "p2", body(r#"{"title":"x"}"#, OpPath::Update))
1758 .await
1759 .unwrap_err()
1760 .code,
1761 ErrorCode::ObjectNotFound
1762 );
1763 }
1764
1765 fn protected_storage() -> FakeStorage {
1770 FakeStorage::new()
1771 .with_schema(
1772 default_schema("Post")
1773 .with_field("secret", FieldType::String)
1774 .with_field("title", FieldType::String)
1775 .with_clp(clp(r#"{"protectedFields":{"*":["secret"]}}"#)),
1776 )
1777 .with_row(
1778 "Post",
1779 row(vec![
1780 ("objectId", ParseValue::String("p1".into())),
1781 ("title", ParseValue::String("t".into())),
1782 ("secret", ParseValue::String("s".into())),
1783 ]),
1784 )
1785 }
1786
1787 #[tokio::test]
1788 async fn a_protected_field_is_absent_from_a_read_and_present_for_master() {
1789 let storage = protected_storage();
1790 let snap = snapshot(&storage).await;
1791 let options = opts();
1792
1793 let anon = AclScope::Anonymous;
1794 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1795 let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1796 .await
1797 .expect("find");
1798 assert!(results[0].get("secret").is_none());
1799 assert!(results[0].get("title").is_some());
1800
1801 let master = AclScope::Unrestricted;
1802 let ctx = Ctx::new(&storage, &snap, &master, &options);
1803 let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1804 .await
1805 .expect("find");
1806 assert!(results[0].get("secret").is_some());
1807 }
1808
1809 #[tokio::test]
1811 async fn querying_or_ordering_by_a_protected_field_is_forbidden() {
1812 let storage = protected_storage();
1813 let snap = snapshot(&storage).await;
1814 let options = opts();
1815 let anon = AclScope::Anonymous;
1816 let ctx = Ctx::new(&storage, &snap, &anon, &options);
1817
1818 let e = find(
1819 &ctx,
1820 "Post",
1821 where_(r#"{"secret":"s"}"#),
1822 FindOptions::default(),
1823 )
1824 .await
1825 .unwrap_err();
1826 assert_eq!(e.code, ErrorCode::OperationForbidden);
1827 assert_eq!(e.message, "Permission denied");
1828
1829 let disclosing = disclosing_opts();
1830 let disclosed = Ctx::new(&storage, &snap, &anon, &disclosing);
1831 assert_eq!(
1832 find(
1833 &disclosed,
1834 "Post",
1835 where_(r#"{"secret":"s"}"#),
1836 FindOptions::default()
1837 )
1838 .await
1839 .unwrap_err()
1840 .message,
1841 "This user is not allowed to query secret on class Post"
1842 );
1843
1844 assert_eq!(
1846 find(
1847 &ctx,
1848 "Post",
1849 where_(r#"{"$or":[{"secret.a":"s"}]}"#),
1850 FindOptions::default()
1851 )
1852 .await
1853 .unwrap_err()
1854 .code,
1855 ErrorCode::OperationForbidden
1856 );
1857
1858 let sorted = FindOptions {
1859 order: vec![("secret".to_string(), SortDirection::Ascending)],
1860 ..Default::default()
1861 };
1862 let e = find(&ctx, "Post", ParsedWhere::default(), sorted.clone())
1863 .await
1864 .unwrap_err();
1865 assert_eq!(e.code, ErrorCode::OperationForbidden);
1866 assert_eq!(e.message, "Permission denied");
1867 assert_eq!(
1868 find(&disclosed, "Post", ParsedWhere::default(), sorted)
1869 .await
1870 .unwrap_err()
1871 .message,
1872 "This user is not allowed to sort by secret on class Post"
1873 );
1874
1875 let master = AclScope::Unrestricted;
1877 let ctx = Ctx::new(&storage, &snap, &master, &options);
1878 assert!(find(
1879 &ctx,
1880 "Post",
1881 where_(r#"{"secret":"s"}"#),
1882 FindOptions::default()
1883 )
1884 .await
1885 .is_ok());
1886 }
1887
1888 #[tokio::test]
1893 async fn an_acl_hides_a_row_from_everyone_but_its_principals() {
1894 let storage = FakeStorage::new()
1895 .with_schema(default_schema("Post"))
1896 .with_row(
1897 "Post",
1898 row(vec![
1899 ("objectId", ParseValue::String("private".into())),
1900 ("_rperm", strings(&["u1", "role:Admins"])),
1901 ("_wperm", strings(&["u1"])),
1902 ]),
1903 )
1904 .with_row(
1905 "Post",
1906 row(vec![("objectId", ParseValue::String("public".into()))]),
1907 );
1908 let snap = snapshot(&storage).await;
1909 let options = opts();
1910
1911 let owner = AclScope::user("u1", vec![]).expect("scope");
1912 let ctx = Ctx::new(&storage, &snap, &owner, &options);
1913 assert_eq!(
1914 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1915 .await
1916 .expect("find")
1917 .len(),
1918 2,
1919 "assert first that the owner can see its own row"
1920 );
1921
1922 let admin = AclScope::user("u2", vec!["Admins".into()]).expect("scope");
1923 let ctx = Ctx::new(&storage, &snap, &admin, &options);
1924 assert_eq!(
1925 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1926 .await
1927 .expect("find")
1928 .len(),
1929 2,
1930 "a role: entry in _rperm must match a member"
1931 );
1932
1933 let stranger = AclScope::user("u3", vec![]).expect("scope");
1934 let ctx = Ctx::new(&storage, &snap, &stranger, &options);
1935 let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1936 .await
1937 .expect("find");
1938 assert_eq!(results.len(), 1);
1939 assert!(
1940 matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "public")
1941 );
1942 assert_eq!(
1943 update(
1944 &ctx,
1945 "Post",
1946 "private",
1947 body(r#"{"title":"x"}"#, OpPath::Update)
1948 )
1949 .await
1950 .unwrap_err()
1951 .code,
1952 ErrorCode::ObjectNotFound
1953 );
1954 assert_eq!(
1955 delete(&ctx, "Post", "private").await.unwrap_err().code,
1956 ErrorCode::ObjectNotFound
1957 );
1958 }
1959
1960 #[tokio::test]
1973 async fn a_declared_default_acl_isolates_the_creator_without_locking_them_out() {
1974 let storage = FakeStorage::new().with_schema(
1975 default_schema("Post")
1976 .with_clp(clp(r#"{"ACL":{"currentUser":{"read":true,"write":true}}}"#)),
1977 );
1978 let snap = snapshot(&storage).await;
1979 let options = opts();
1980
1981 let a = AclScope::user("userA", vec![]).expect("scope");
1982 let ctx = Ctx::new(&storage, &snap, &a, &options);
1983 let created = create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1984 .await
1985 .expect("create");
1986
1987 assert_eq!(
1988 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1989 .await
1990 .expect("find")
1991 .len(),
1992 1,
1993 "the creator must still be able to read its own object"
1994 );
1995 assert!(
1996 update(
1997 &ctx,
1998 "Post",
1999 &created.object_id,
2000 body(r#"{"title":"y"}"#, OpPath::Update)
2001 )
2002 .await
2003 .is_ok(),
2004 "and to write it: _wperm is a separate column and can be wrong on its own"
2005 );
2006
2007 let b = AclScope::user("userB", vec![]).expect("scope");
2008 let ctx = Ctx::new(&storage, &snap, &b, &options);
2009 assert!(
2010 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
2011 .await
2012 .expect("find")
2013 .is_empty(),
2014 "0.2.0 returned this object to every caller"
2015 );
2016 assert_eq!(
2017 update(
2018 &ctx,
2019 "Post",
2020 &created.object_id,
2021 body(r#"{"title":"z"}"#, OpPath::Update)
2022 )
2023 .await
2024 .unwrap_err()
2025 .code,
2026 ErrorCode::ObjectNotFound
2027 );
2028 }
2029
2030 #[tokio::test]
2033 async fn a_class_with_no_declared_acl_still_creates_public_rows() {
2034 let storage = FakeStorage::new().with_schema(default_schema("Post"));
2035 let snap = snapshot(&storage).await;
2036 let options = opts();
2037
2038 let a = AclScope::user("userA", vec![]).expect("scope");
2039 let ctx = Ctx::new(&storage, &snap, &a, &options);
2040 create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
2041 .await
2042 .expect("create");
2043
2044 let b = AclScope::user("userB", vec![]).expect("scope");
2045 let ctx = Ctx::new(&storage, &snap, &b, &options);
2046 assert_eq!(
2047 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
2048 .await
2049 .expect("find")
2050 .len(),
2051 1
2052 );
2053 }
2054
2055 #[tokio::test]
2059 async fn the_default_applies_on_create_and_never_on_update() {
2060 let storage = FakeStorage::new().with_schema(
2061 default_schema("Post")
2062 .with_clp(clp(r#"{"ACL":{"currentUser":{"read":true,"write":true}}}"#)),
2063 );
2064 let snap = snapshot(&storage).await;
2065 let options = opts();
2066
2067 let a = AclScope::user("userA", vec![]).expect("scope");
2069 let ctx = Ctx::new(&storage, &snap, &a, &options);
2070 let created = create(
2071 &ctx,
2072 "Post",
2073 body(
2074 r#"{"title":"x","ACL":{"userA":{"read":true,"write":true},"userB":{"read":true}}}"#,
2075 OpPath::Create,
2076 ),
2077 )
2078 .await
2079 .expect("create");
2080
2081 let b = AclScope::user("userB", vec![]).expect("scope");
2082 let b_ctx = Ctx::new(&storage, &snap, &b, &options);
2083 assert_eq!(
2084 find(
2085 &b_ctx,
2086 "Post",
2087 ParsedWhere::default(),
2088 FindOptions::default()
2089 )
2090 .await
2091 .expect("find")
2092 .len(),
2093 1,
2094 "the client's own ACL must win over the class default on create"
2095 );
2096
2097 update(
2099 &ctx,
2100 "Post",
2101 &created.object_id,
2102 body(r#"{"title":"y"}"#, OpPath::Update),
2103 )
2104 .await
2105 .expect("update");
2106 assert_eq!(
2107 find(
2108 &b_ctx,
2109 "Post",
2110 ParsedWhere::default(),
2111 FindOptions::default()
2112 )
2113 .await
2114 .expect("find")
2115 .len(),
2116 1,
2117 "the explicitly set ACL must survive an unrelated update"
2118 );
2119 }
2120
2121 #[tokio::test]
2125 async fn a_falsy_acl_on_the_body_does_not_suppress_the_default() {
2126 let storage = FakeStorage::new().with_schema(
2127 default_schema("Post").with_clp(clp(r#"{"ACL":{"currentUser":{"read":true}}}"#)),
2128 );
2129 let snap = snapshot(&storage).await;
2130 let options = opts();
2131
2132 let a = AclScope::user("userA", vec![]).expect("scope");
2133 let ctx = Ctx::new(&storage, &snap, &a, &options);
2134 create(
2135 &ctx,
2136 "Post",
2137 body(r#"{"title":"x","ACL":null}"#, OpPath::Create),
2138 )
2139 .await
2140 .expect("create");
2141
2142 let b = AclScope::user("userB", vec![]).expect("scope");
2143 let ctx = Ctx::new(&storage, &snap, &b, &options);
2144 assert!(
2145 find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
2146 .await
2147 .expect("find")
2148 .is_empty()
2149 );
2150 }
2151
2152 #[tokio::test]
2162 async fn an_acl_operation_on_create_writes_empty_columns_rather_than_none() {
2163 for literal in [
2164 r#"{"title":"x","ACL":{"__op":"Delete"}}"#,
2165 r#"{"title":"x","ACL":{"__op":"Increment","amount":1}}"#,
2166 ] {
2167 let storage = FakeStorage::new().with_schema(default_schema("Post"));
2168 let snap = snapshot(&storage).await;
2169 let options = opts();
2170 let master = AclScope::Unrestricted;
2171 let ctx = Ctx::new(&storage, &snap, &master, &options);
2172
2173 create(&ctx, "Post", body(literal, OpPath::Create))
2174 .await
2175 .expect("create");
2176
2177 let rows = storage.rows("Post");
2178 assert_eq!(rows.len(), 1, "{literal}");
2179 for column in ["_rperm", "_wperm"] {
2180 assert!(
2181 matches!(rows[0].get(column), Some(ParseValue::Array(a)) if a.is_empty()),
2182 "{literal} must write an empty {column}, got {:?}",
2183 rows[0].get(column)
2184 );
2185 }
2186 }
2187 }
2188
2189 #[tokio::test]
2192 async fn a_create_with_no_acl_still_writes_no_columns() {
2193 let storage = FakeStorage::new().with_schema(default_schema("Post"));
2194 let snap = snapshot(&storage).await;
2195 let options = opts();
2196 let master = AclScope::Unrestricted;
2197 let ctx = Ctx::new(&storage, &snap, &master, &options);
2198
2199 create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
2200 .await
2201 .expect("create");
2202
2203 let rows = storage.rows("Post");
2204 assert!(rows[0].get("_rperm").is_none());
2205 assert!(rows[0].get("_wperm").is_none());
2206 }
2207
2208 #[tokio::test]
2213 async fn a_declared_default_does_not_satisfy_roles_required_acl() {
2214 let storage = FakeStorage::new().with_schema(
2215 default_schema("_Role").with_clp(clp(r#"{"ACL":{"currentUser":{"read":true}}}"#)),
2216 );
2217 let snap = snapshot(&storage).await;
2218 let options = opts();
2219 let master = AclScope::Unrestricted;
2220 let ctx = Ctx::new(&storage, &snap, &master, &options);
2221
2222 let e = create(&ctx, "_Role", body(r#"{"name":"Admins"}"#, OpPath::Create))
2223 .await
2224 .unwrap_err();
2225 assert_eq!(e.code, ErrorCode::IncorrectType);
2226 assert_eq!(e.message, "ACL is required.");
2227 assert!(storage.rows("_Role").is_empty());
2228 }
2229
2230 #[tokio::test]
2236 async fn operations_reach_storage_as_operations() {
2237 let storage = FakeStorage::new();
2238 let snap = snapshot(&storage).await;
2239 let options = opts();
2240 let master = AclScope::Unrestricted;
2241 let ctx = Ctx::new(&storage, &snap, &master, &options);
2242
2243 let created = create(
2244 &ctx,
2245 "Post",
2246 body(
2247 r#"{"views":{"__op":"Increment","amount":2},"tags":{"__op":"Add","objects":["a"]}}"#,
2248 OpPath::Create,
2249 ),
2250 )
2251 .await
2252 .expect("create");
2253
2254 let stored = storage.rows("Post");
2255 assert!(
2256 matches!(stored[0].get("views"), Some(ParseValue::Number(n)) if *n == 2.0),
2257 "an Increment must be flattened to a number, not stored as an op envelope"
2258 );
2259 assert!(matches!(stored[0].get("tags"), Some(ParseValue::Array(a)) if a.len() == 1));
2260 assert!(matches!(created.echoed.get("views"), Some(ParseValue::Number(n)) if *n == 2.0));
2261
2262 let snap = snapshot(&storage).await;
2263 let ctx = Ctx::new(&storage, &snap, &master, &options);
2264 let updated = update(
2265 &ctx,
2266 "Post",
2267 &created.object_id,
2268 body(
2269 r#"{"views":{"__op":"Increment","amount":3},"title":"plain"}"#,
2270 OpPath::Update,
2271 ),
2272 )
2273 .await
2274 .expect("update");
2275 assert!(
2276 matches!(updated.echoed.get("views"), Some(ParseValue::Number(n)) if *n == 5.0),
2277 "the response carries the post-update value"
2278 );
2279 assert!(
2280 updated.echoed.get("title").is_none(),
2281 "a plain set is not echoed"
2282 );
2283 assert!(
2284 matches!(storage.rows("Post")[0].get("views"), Some(ParseValue::Number(n)) if *n == 5.0)
2285 );
2286 }
2287
2288 #[tokio::test]
2289 async fn a_delete_op_unsets_the_field_and_echoes_nothing() {
2290 let storage = FakeStorage::new()
2291 .with_schema(default_schema("Post").with_field("title", FieldType::String))
2292 .with_row(
2293 "Post",
2294 row(vec![
2295 ("objectId", ParseValue::String("p1".into())),
2296 ("title", ParseValue::String("t".into())),
2297 ]),
2298 );
2299 let snap = snapshot(&storage).await;
2300 let options = opts();
2301 let master = AclScope::Unrestricted;
2302 let ctx = Ctx::new(&storage, &snap, &master, &options);
2303
2304 let response = update(
2305 &ctx,
2306 "Post",
2307 "p1",
2308 body(r#"{"title":{"__op":"Delete"}}"#, OpPath::Update),
2309 )
2310 .await
2311 .expect("update");
2312 assert!(response.echoed.is_empty());
2313 assert!(storage.rows("Post")[0].get("title").is_none());
2314 }
2315
2316 #[tokio::test]
2319 async fn a_type_conflict_fails_the_write_before_the_row_is_inserted() {
2320 let storage = FakeStorage::new()
2321 .with_schema(default_schema("Post").with_field("views", FieldType::Number));
2322 let snap = SchemaSnapshot::from_classes(vec![default_schema("Post")]);
2323 let options = opts();
2324 let master = AclScope::Unrestricted;
2325 let ctx = Ctx::new(&storage, &snap, &master, &options);
2326
2327 let e = create(&ctx, "Post", body(r#"{"views":"text"}"#, OpPath::Create))
2330 .await
2331 .unwrap_err();
2332 assert_eq!(e.code, ErrorCode::IncorrectType);
2333 assert_eq!(
2334 e.message,
2335 "schema mismatch for Post.views; expected Number but got String"
2336 );
2337 assert!(storage.rows("Post").is_empty(), "no row was inserted");
2338 }
2339
2340 #[tokio::test]
2341 async fn a_write_of_only_nulls_still_creates_the_class() {
2342 let storage = FakeStorage::new();
2343 let snap = snapshot(&storage).await;
2344 let options = opts();
2345 let master = AclScope::Unrestricted;
2346 let ctx = Ctx::new(&storage, &snap, &master, &options);
2347
2348 create(&ctx, "Post", body(r#"{"nothing":null}"#, OpPath::Create))
2349 .await
2350 .expect("create");
2351 let schema = storage
2352 .schema("Post")
2353 .expect("a null-only write must still leave a schema row behind");
2354 assert!(schema.field("objectId").is_some());
2355 assert!(schema.field("nothing").is_none(), "null infers no type");
2356 }
2357
2358 #[tokio::test]
2363 async fn a_relation_write_lands_in_the_join_table_and_reads_back_through_related_to() {
2364 let storage = FakeStorage::new()
2365 .with_schema(default_schema("_Role"))
2366 .with_schema(default_schema("_User"))
2367 .with_row(
2368 "_User",
2369 row(vec![("objectId", ParseValue::String("u1".into()))]),
2370 );
2371 let snap = snapshot(&storage).await;
2372 let options = opts();
2373 let master = AclScope::Unrestricted;
2374 let ctx = Ctx::new(&storage, &snap, &master, &options);
2375
2376 let created = create(
2377 &ctx,
2378 "_Role",
2379 body(
2380 r#"{"name":"admins","ACL":{"*":{"read":true}},
2381 "users":{"__op":"AddRelation","objects":[
2382 {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2383 OpPath::Create,
2384 ),
2385 )
2386 .await
2387 .expect("create");
2388
2389 let stored = storage.rows("_Role");
2390 assert!(
2391 stored[0].get("users").is_none(),
2392 "a Relation field has no column"
2393 );
2394 let joins = storage.rows("_Join:users:_Role");
2395 assert_eq!(joins.len(), 1);
2396 assert!(matches!(joins[0].get("relatedId"), Some(ParseValue::String(id)) if id == "u1"));
2397 assert!(
2398 matches!(joins[0].get("owningId"), Some(ParseValue::String(id)) if *id == created.object_id)
2399 );
2400 assert!(
2401 storage.schema("_Join:users:_Role").is_none(),
2402 "a join collection has no _SCHEMA row"
2403 );
2404
2405 let snap = snapshot(&storage).await;
2407 let ctx = Ctx::new(&storage, &snap, &master, &options);
2408 update(
2409 &ctx,
2410 "_Role",
2411 &created.object_id,
2412 body(
2413 r#"{"users":{"__op":"AddRelation","objects":[
2414 {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2415 OpPath::Update,
2416 ),
2417 )
2418 .await
2419 .expect("update");
2420 assert_eq!(storage.rows("_Join:users:_Role").len(), 1);
2421
2422 let query = format!(
2424 r#"{{"$relatedTo":{{"object":{{"__type":"Pointer","className":"_Role","objectId":"{}"}},"key":"users"}}}}"#,
2425 created.object_id
2426 );
2427 let members = find(&ctx, "_User", where_(&query), FindOptions::default())
2428 .await
2429 .expect("find");
2430 assert_eq!(members.len(), 1);
2431
2432 update(
2434 &ctx,
2435 "_Role",
2436 &created.object_id,
2437 body(
2438 r#"{"users":{"__op":"RemoveRelation","objects":[
2439 {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2440 OpPath::Update,
2441 ),
2442 )
2443 .await
2444 .expect("update");
2445 assert!(storage.rows("_Join:users:_Role").is_empty());
2446 }
2447
2448 #[tokio::test]
2451 async fn a_related_to_the_caller_cannot_read_yields_empty_rather_than_an_error() {
2452 let storage = FakeStorage::new()
2453 .with_schema(default_schema("_Role"))
2454 .with_schema(default_schema("_User"))
2455 .with_row(
2456 "_Role",
2457 row(vec![
2458 ("objectId", ParseValue::String("r1".into())),
2459 ("_rperm", strings(&["u2"])),
2460 ]),
2461 )
2462 .with_row(
2463 "_User",
2464 row(vec![("objectId", ParseValue::String("u1".into()))]),
2465 )
2466 .with_row(
2467 "_Join:users:_Role",
2468 row(vec![
2469 ("relatedId", ParseValue::String("u1".into())),
2470 ("owningId", ParseValue::String("r1".into())),
2471 ]),
2472 );
2473 let snap = snapshot(&storage).await;
2474 let options = opts();
2475 let query = r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#;
2476
2477 let outsider = AclScope::user("u3", vec![]).expect("scope");
2478 let ctx = Ctx::new(&storage, &snap, &outsider, &options);
2479 let results = find(&ctx, "_User", where_(query), FindOptions::default())
2480 .await
2481 .expect("a denied relation is empty, not an error");
2482 assert!(results.is_empty());
2483
2484 let insider = AclScope::user("u2", vec![]).expect("scope");
2487 let ctx = Ctx::new(&storage, &snap, &insider, &options);
2488 assert_eq!(
2489 find(&ctx, "_User", where_(query), FindOptions::default())
2490 .await
2491 .expect("find")
2492 .len(),
2493 1
2494 );
2495 }
2496
2497 #[tokio::test]
2500 async fn a_related_to_on_a_protected_key_is_forbidden() {
2501 let storage = FakeStorage::new()
2502 .with_schema(default_schema("_User"))
2503 .with_schema(
2504 default_schema("_Role")
2505 .with_field(
2506 "users",
2507 FieldType::Relation {
2508 target_class: "_User".into(),
2509 },
2510 )
2511 .with_clp(clp(r#"{"protectedFields":{"*":["users"]}}"#)),
2512 )
2513 .with_row(
2514 "_Role",
2515 row(vec![("objectId", ParseValue::String("r1".into()))]),
2516 );
2517 let snap = snapshot(&storage).await;
2518 let options = opts();
2519 let user = AclScope::user("u1", vec![]).expect("scope");
2520 let ctx = Ctx::new(&storage, &snap, &user, &options);
2521 let query = r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#;
2522 let e = find(&ctx, "_User", where_(query), FindOptions::default())
2523 .await
2524 .unwrap_err();
2525 assert_eq!(e.code, ErrorCode::OperationForbidden);
2526 assert_eq!(e.message, "Permission denied");
2527
2528 let disclosing = disclosing_opts();
2529 let disclosed = Ctx::new(&storage, &snap, &user, &disclosing);
2530 assert_eq!(
2531 find(&disclosed, "_User", where_(query), FindOptions::default())
2532 .await
2533 .unwrap_err()
2534 .message,
2535 "This user is not allowed to query users on class _Role"
2536 );
2537 }
2538
2539 #[tokio::test]
2540 async fn a_constraint_on_a_relation_field_is_the_reverse_join() {
2541 let storage = FakeStorage::new()
2542 .with_schema(default_schema("_User"))
2543 .with_schema(default_schema("_Role").with_field(
2544 "users",
2545 FieldType::Relation {
2546 target_class: "_User".into(),
2547 },
2548 ))
2549 .with_row(
2550 "_Role",
2551 row(vec![("objectId", ParseValue::String("r1".into()))]),
2552 )
2553 .with_row(
2554 "_Role",
2555 row(vec![("objectId", ParseValue::String("r2".into()))]),
2556 )
2557 .with_row(
2558 "_Join:users:_Role",
2559 row(vec![
2560 ("relatedId", ParseValue::String("u1".into())),
2561 ("owningId", ParseValue::String("r1".into())),
2562 ]),
2563 );
2564 let snap = snapshot(&storage).await;
2565 let options = opts();
2566 let master = AclScope::Unrestricted;
2567 let ctx = Ctx::new(&storage, &snap, &master, &options);
2568
2569 let results = find(
2570 &ctx,
2571 "_Role",
2572 where_(r#"{"users":{"__type":"Pointer","className":"_User","objectId":"u1"}}"#),
2573 FindOptions::default(),
2574 )
2575 .await
2576 .expect("find");
2577 assert_eq!(results.len(), 1);
2578 assert!(matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "r1"));
2579 }
2580
2581 #[tokio::test]
2588 async fn include_applies_the_target_class_acl() {
2589 let storage = FakeStorage::new()
2590 .with_schema(default_schema("_User").with_field("nickname", FieldType::String))
2591 .with_schema(default_schema("Post").with_field(
2592 "author",
2593 FieldType::Pointer {
2594 target_class: "_User".into(),
2595 },
2596 ))
2597 .with_row(
2598 "Post",
2599 row(vec![
2600 ("objectId", ParseValue::String("p1".into())),
2601 ("author", pointer("_User", "u1")),
2602 ]),
2603 )
2604 .with_row(
2605 "_User",
2606 row(vec![
2607 ("objectId", ParseValue::String("u1".into())),
2608 ("nickname", ParseValue::String("nick".into())),
2609 ("_hashed_password", ParseValue::String("hash".into())),
2610 ("sessionToken", ParseValue::String("r:t".into())),
2611 ("_rperm", strings(&["u1"])),
2612 ]),
2613 );
2614 let snap = snapshot(&storage).await;
2615 let options = opts();
2616 let include = FindOptions {
2617 include: vec![vec!["author".to_string()]],
2618 ..Default::default()
2619 };
2620
2621 let anon = AclScope::Anonymous;
2622 let ctx = Ctx::new(&storage, &snap, &anon, &options);
2623 let results = find(&ctx, "Post", ParsedWhere::default(), include.clone())
2624 .await
2625 .expect("find");
2626 assert_eq!(results.len(), 1);
2627 assert!(
2628 results[0].get("author").is_none(),
2629 "an unreadable pointer is dropped rather than expanded or left as a pointer"
2630 );
2631
2632 let owner = AclScope::user("u1", vec![]).expect("scope");
2633 let ctx = Ctx::new(&storage, &snap, &owner, &options);
2634 let results = find(&ctx, "Post", ParsedWhere::default(), include)
2635 .await
2636 .expect("find");
2637 match results[0].get("author") {
2638 Some(ParseValue::Object(author)) => {
2639 assert!(
2640 matches!(author.get("nickname"), Some(ParseValue::String(n)) if n == "nick")
2641 );
2642 assert!(author.get("_hashed_password").is_none());
2643 assert!(author.get("sessionToken").is_none());
2644 assert!(
2645 matches!(author.get("__type"), Some(ParseValue::String(t)) if t == "Object")
2646 );
2647 }
2648 other => panic!("expected an expanded author, got {other:?}"),
2649 }
2650 }
2651
2652 #[tokio::test]
2653 async fn a_dotted_include_resolves_parents_before_children() {
2654 let storage = FakeStorage::new()
2655 .with_schema(default_schema("Company").with_field("name", FieldType::String))
2656 .with_schema(default_schema("_User").with_field(
2657 "company",
2658 FieldType::Pointer {
2659 target_class: "Company".into(),
2660 },
2661 ))
2662 .with_schema(default_schema("Post").with_field(
2663 "author",
2664 FieldType::Pointer {
2665 target_class: "_User".into(),
2666 },
2667 ))
2668 .with_row(
2669 "Post",
2670 row(vec![
2671 ("objectId", ParseValue::String("p1".into())),
2672 ("author", pointer("_User", "u1")),
2673 ]),
2674 )
2675 .with_row(
2676 "_User",
2677 row(vec![
2678 ("objectId", ParseValue::String("u1".into())),
2679 ("company", pointer("Company", "c1")),
2680 ]),
2681 )
2682 .with_row(
2683 "Company",
2684 row(vec![
2685 ("objectId", ParseValue::String("c1".into())),
2686 ("name", ParseValue::String("Acme".into())),
2687 ]),
2688 );
2689 let snap = snapshot(&storage).await;
2690 let options = opts();
2691 let anon = AclScope::Anonymous;
2692 let ctx = Ctx::new(&storage, &snap, &anon, &options);
2693
2694 let results = find(
2695 &ctx,
2696 "Post",
2697 ParsedWhere::default(),
2698 FindOptions {
2699 include: crate::query_parse::parse_include("author.company").expect("include"),
2700 ..Default::default()
2701 },
2702 )
2703 .await
2704 .expect("find");
2705
2706 let Some(ParseValue::Object(author)) = results[0].get("author") else {
2707 panic!("author should be expanded");
2708 };
2709 let Some(ParseValue::Object(company)) = author.get("company") else {
2710 panic!("company should be expanded");
2711 };
2712 assert!(matches!(company.get("name"), Some(ParseValue::String(n)) if n == "Acme"));
2713 }
2714
2715 #[tokio::test]
2719 async fn a_client_cannot_bring_a_class_into_existence_at_the_default() {
2720 let storage = FakeStorage::new();
2721 let snap = snapshot(&storage).await;
2722 let options = opts();
2723 let anon = AclScope::Anonymous;
2724 let ctx = Ctx::new(&storage, &snap, &anon, &options);
2725
2726 let err = create(&ctx, "BrandNew", body(r#"{"x":1}"#, OpPath::Create))
2727 .await
2728 .expect_err("a client must not create a class at the default");
2729 assert_eq!(err.code, ErrorCode::OperationForbidden);
2730 assert!(
2731 storage.schema("BrandNew").is_none(),
2732 "the refusal must leave no _SCHEMA row behind"
2733 );
2734 }
2735
2736 #[tokio::test]
2739 async fn master_the_option_and_the_system_classes_are_all_exempt() {
2740 for (label, scope, allow, class) in [
2741 ("option on", AclScope::Anonymous, true, "BrandNew"),
2742 ("master", AclScope::Unrestricted, false, "BrandNew"),
2743 ("system class", AclScope::Anonymous, false, "_User"),
2744 ] {
2745 let storage = FakeStorage::new();
2746 let snap = snapshot(&storage).await;
2747 let options = PermissionOptions {
2748 allow_client_class_creation: allow,
2749 ..PermissionOptions::default()
2750 };
2751 let ctx = Ctx::new(&storage, &snap, &scope, &options);
2752 create(&ctx, class, body(r#"{"x":1}"#, OpPath::Create))
2753 .await
2754 .unwrap_or_else(|e| panic!("{label} should be allowed to create {class}: {e:?}"));
2755 }
2756 }
2757}
2758
2759#[cfg(test)]
2760mod relation_schema_tests {
2761 use super::tests_support::*;
2762 use super::*;
2763 use crate::testing::FakeStorage;
2764 use parse_rust_core::op::OpPath;
2765
2766 #[tokio::test]
2770 async fn a_relation_op_reserves_the_field_type_before_it_is_stripped() {
2771 let storage = FakeStorage::new()
2772 .with_schema(default_schema("_User"))
2773 .with_row("_User", single("objectId", ParseValue::String("u1".into())));
2774 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2775 let options = PermissionOptions::default();
2776 let master = AclScope::Unrestricted;
2777 let ctx = Ctx::new(&storage, &snap, &master, &options);
2778
2779 let created = create(
2780 &ctx,
2781 "Team",
2782 decode(
2783 r#"{"name":"core","members":{"__op":"AddRelation","objects":[
2784 {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2785 OpPath::Create,
2786 ),
2787 )
2788 .await
2789 .expect("create");
2790
2791 let schema = storage.schema("Team").expect("class reserved");
2792 assert_eq!(
2793 schema.field("members"),
2794 Some(&FieldType::Relation {
2795 target_class: "_User".into()
2796 }),
2797 "without this a $relatedTo against Team.members resolves to nothing forever"
2798 );
2799 assert!(storage.rows("Team")[0].get("members").is_none());
2800 assert_eq!(storage.rows("_Join:members:Team").len(), 1);
2801
2802 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2804 let ctx = Ctx::new(&storage, &snap, &master, &options);
2805 let query = format!(
2806 r#"{{"$relatedTo":{{"object":{{"__type":"Pointer","className":"Team","objectId":"{}"}},"key":"members"}}}}"#,
2807 created.object_id
2808 );
2809 assert_eq!(
2810 find(
2811 &ctx,
2812 "_User",
2813 parse_json_where(&query),
2814 FindOptions::default()
2815 )
2816 .await
2817 .expect("find")
2818 .len(),
2819 1
2820 );
2821 }
2822}
2823
2824#[cfg(test)]
2825mod tests_support {
2826 use super::*;
2827 use crate::query_parse::parse_where;
2828 use crate::write::decode_write_body;
2829 use parse_rust_core::op::OpPath;
2830
2831 pub fn single(key: &str, value: ParseValue) -> ParseMap {
2832 let mut m = ParseMap::new();
2833 m.insert(key.to_string(), value);
2834 m
2835 }
2836
2837 pub fn decode(json: &str, path: OpPath) -> WriteBody {
2838 decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
2839 }
2840
2841 pub fn parse_json_where(json: &str) -> ParsedWhere {
2842 parse_where(&serde_json::from_str(json).expect("test literal")).expect("parse")
2843 }
2844}
2845
2846#[cfg(test)]
2847mod write_edge_tests {
2848 use super::tests_support::*;
2849 use super::*;
2850 use crate::testing::FakeStorage;
2851 use parse_rust_core::op::OpPath;
2852
2853 #[tokio::test]
2862 async fn a_falsy_acl_on_an_update_does_not_clear_the_permissions() {
2863 for body in [
2864 r#"{"ACL":null,"title":"t"}"#,
2865 r#"{"ACL":false,"title":"t"}"#,
2866 r#"{"ACL":0,"title":"t"}"#,
2867 r#"{"ACL":"","title":"t"}"#,
2868 ] {
2869 assert_falsy_acl_preserves_permissions(body).await;
2870 }
2871 }
2872
2873 async fn assert_falsy_acl_preserves_permissions(body: &str) {
2874 let mut existing = single("objectId", ParseValue::String("p1".into()));
2875 existing.insert(
2876 "_rperm".to_string(),
2877 ParseValue::Array(vec![ParseValue::String("u1".into())]),
2878 );
2879 existing.insert(
2880 "_wperm".to_string(),
2881 ParseValue::Array(vec![ParseValue::String("u1".into())]),
2882 );
2883 let storage = FakeStorage::new()
2884 .with_schema(default_schema("Post").with_field("title", FieldType::String))
2885 .with_row("Post", existing);
2886 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2887 let options = PermissionOptions::default();
2888 let master = AclScope::Unrestricted;
2889 let ctx = Ctx::new(&storage, &snap, &master, &options);
2890
2891 update(&ctx, "Post", "p1", decode(body, OpPath::Update))
2892 .await
2893 .expect("update");
2894
2895 let stored = &storage.rows("Post")[0];
2896 assert!(
2897 matches!(stored.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 1),
2898 "the existing permissions must survive a falsy ACL: {body}"
2899 );
2900 }
2901
2902 #[tokio::test]
2912 async fn an_update_to_a_missing_class_creates_the_class_and_then_finds_nothing() {
2913 for body in [r#"{}"#, r#"{"title":"a"}"#, r#"{"bad-key":1}"#] {
2918 let storage = FakeStorage::new();
2919 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2920 let options = PermissionOptions::default();
2921 let master = AclScope::Unrestricted;
2922 let ctx = Ctx::new(&storage, &snap, &master, &options);
2923
2924 let e = update(&ctx, "Ghost", "p1", decode(body, OpPath::Update))
2925 .await
2926 .unwrap_err();
2927 let expected = if body.contains("bad-key") {
2928 ErrorCode::InvalidKeyName
2929 } else {
2930 ErrorCode::ObjectNotFound
2931 };
2932 assert_eq!(e.code, expected, "for body {body}");
2933 assert!(
2934 storage.schema("Ghost").is_some(),
2935 "the schema row survives the failed update, for body {body}"
2936 );
2937 }
2938 }
2939
2940 #[tokio::test]
2942 async fn an_acl_on_an_update_replaces_both_columns() {
2943 let storage = FakeStorage::new()
2944 .with_schema(default_schema("Post"))
2945 .with_row("Post", single("objectId", ParseValue::String("p1".into())));
2946 let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2947 let options = PermissionOptions::default();
2948 let master = AclScope::Unrestricted;
2949 let ctx = Ctx::new(&storage, &snap, &master, &options);
2950
2951 update(
2952 &ctx,
2953 "Post",
2954 "p1",
2955 decode(
2956 r#"{"ACL":{"u1":{"read":true,"write":true},"*":{"read":true}}}"#,
2957 OpPath::Update,
2958 ),
2959 )
2960 .await
2961 .expect("update");
2962
2963 let stored = &storage.rows("Post")[0];
2964 assert!(stored.get("ACL").is_none(), "ACL is not a stored column");
2965 assert!(matches!(stored.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 2));
2966 assert!(matches!(stored.get("_wperm"), Some(ParseValue::Array(a)) if a.len() == 1));
2967 }
2968}