Skip to main content

parse_rust_rest/
pipeline.rs

1//! The read and write pipelines.
2//!
3//! Generic over [`StorageAdapter`] rather than taking a `dyn`, so the storage boundary costs
4//! nothing at runtime and a future Postgres adapter drops in by type rather than by trait object.
5//!
6//! Every entry point takes a [`SchemaSnapshot`] and an [`AclScope`] and runs the stages in
7//! upstream's order. The read order is `DatabaseController.js:1418-1598`:
8//!
9//! load the schema, once per request; resolve the class, a missing one behaving as empty;
10//! validate the sort, dropping unknown keys; the CLP gate; `$relatedTo` with its authorization;
11//! relation-field constraints; pointer permissions; protected fields; the deny check; the ACL
12//! clause; query validation; dispatch; raise the ACL and filter sensitive data.
13//!
14//! Two orderings differ from a naive reading and both are upstream's. `denyProtectedFields` runs
15//! in `RestQuery.execute` *before* the CLP gate (`RestQuery.js:284-288`), so a query naming a
16//! protected field reports that rather than the CLP denial. And `canAddField` runs before the
17//! per-operation gate on a write (`DatabaseController.js:526-536`), so an unauthorized field
18//! addition is reported ahead of an unauthorized create.
19
20use 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
51/// A boxed future, used at the one place the read pipeline is genuinely recursive: `$relatedTo`
52/// authorization reads the owning object through the same pipeline with the same caller.
53type BoxFut<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
54
55/// Everything one request carries into every stage.
56///
57/// The snapshot is taken once and threaded down, so a batch cannot evaluate half its work under
58/// one schema and half under another.
59pub 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    /// Whether this request authenticated with the **maintenance** key rather than the master key.
65    ///
66    /// [`AclScope::Unrestricted`] covers both, because they apply the same ACL treatment: none. But
67    /// they are not the same authority, and at least one decision reads them differently.
68    /// `validateClientClassCreation` exempts master *and* maintenance on a write
69    /// (`RestWrite.js:200-202`) and only master on a read (`RestQuery.js:486-489`), so the read path
70    /// needs to tell them apart and the scope cannot.
71    ///
72    /// A separate flag rather than an `AclScope` variant, deliberately and narrowly: a variant
73    /// would force a master-versus-maintenance judgment at all twenty-five `Unrestricted` sites,
74    /// and only this one is known to differ. The general conflation is recorded in
75    /// the deliberate differences in `CHANGELOG.md`; this closes the case that is known to be
76    /// wrong rather than pretending to close the rest.
77    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    /// Mark the request as maintenance-key authenticated.
97    ///
98    /// Defaults to false so the forty-odd `Ctx::new` call sites, nearly all of them tests, keep
99    /// their signature: a test that does not care about the distinction cannot get it wrong.
100    pub fn maintenance(mut self, yes: bool) -> Self {
101        self.is_maintenance = yes;
102        self
103    }
104}
105
106/// Everything about a read that is not a constraint.
107#[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    /// Subtracted from the projection before it reaches storage, so an adapter only ever sees the
114    /// positive form.
115    pub exclude_keys: Option<Vec<String>>,
116    /// Include paths, every prefix materialized and sorted by depth. Build with
117    /// [`crate::query_parse::parse_include`].
118    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/// What a create returns: `{objectId, createdAt}`, plus the post-write value of any operation the
135/// request carried.
136#[derive(Debug, Clone)]
137pub struct CreateResponse {
138    pub object_id: String,
139    pub created_at: ParseDate,
140    /// Empty unless the body carried an `Add`, `AddUnique`, `Remove` or `Increment`.
141    pub echoed: ParseMap,
142}
143
144/// What an update returns: `{updatedAt}`, plus the same operation echo.
145#[derive(Debug, Clone)]
146pub struct UpdateResponse {
147    pub updated_at: ParseDate,
148    pub echoed: ParseMap,
149}
150
151/// The error both "does not exist" and "you cannot see it" produce.
152///
153/// Upstream conflates them deliberately: distinguishing them would tell an unauthorized caller
154/// that the object exists.
155fn object_not_found() -> ParseError {
156    ParseError::new(ErrorCode::ObjectNotFound, "Object not found.")
157}
158
159// ---------------------------------------------------------------------------------------------
160// Reads
161// ---------------------------------------------------------------------------------------------
162
163/// Find objects, then expand any `include` paths.
164pub 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    // `op` is derived, not passed: a query whose only constraint pins one objectId is a `get` for
171    // CLP purposes (`DatabaseController.js:1412-1413`), so a class that grants `get` and denies
172    // `find` still serves it.
173    let op = derived_op(&where_);
174    let mut results = find_core(
175        ctx,
176        class_name,
177        where_,
178        options.clone(),
179        op,
180        // The method is the route's, not the query's (`rest.js:136`). A pinned `where` narrows
181        // what the CLP is asked about; it does not turn a `find` request into a `get` request.
182        ReadMethod::Find,
183    )
184    .await?;
185    expand_includes(ctx, &mut results, &options).await?;
186    Ok(results)
187}
188
189/// Fetch one object by id.
190pub 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
213/// Count objects.
214pub 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    // A count is served by the find route, so the class-security method is `find` (`rest.js:136`).
221    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        // A count denied by a pointer permission is zero.
233        //
234        // UPSTREAM-QUIRK, deliberately not reproduced: upstream returns the literal `[]` from the
235        // shared deny branch (`DatabaseController.js:1509-1515`) whatever the operation, so a
236        // denied count answers `{"count": []}` on the wire. That is a type confusion rather than
237        // a behavior a client can depend on, and reproducing it would mean giving this function a
238        // return type that can hold an array.
239        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/// `RestQuery.Method` (`RestQuery.js:80-83`): which read this is, as opposed to what the CLP gate
249/// is asked about.
250///
251/// **These are two different questions and upstream answers them from two different places.** The
252/// method comes from the route (`rest.js:136` and `:150` name it literally) or, on the include
253/// path, from how many ids were collected (`RestQuery.js:1250-1251`). The CLP operation is derived
254/// from the query shape instead (`DatabaseController.js:1412-1413`), and the include path pins it
255/// to `get` regardless of the method it just chose (`RestQuery.js:1259`).
256///
257/// Collapsing the two loses `enforceRoleSecurity`'s method-sensitive rules. `_Installation` is the
258/// one that bites: clients may `get` an installation and may not `find` one, so deriving the
259/// method from the query shape hands a client every installation row through either a pinned
260/// `where` or a multi-object `include`.
261#[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
276/// The `get`/`find` distinction upstream derives from the query shape.
277fn 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
294/// A read that survived the authorization stages, or one that did not.
295enum ReadPlan {
296    Run {
297        query: Query,
298        protected: Option<ProtectedFieldPlan>,
299        order: Vec<(String, SortDirection)>,
300    },
301    /// Stage two said deny-all. The caller decides what that looks like on the wire.
302    Denied,
303}
304
305/// The class sessions live in.
306pub const SESSION_CLASS: &str = "_Session";
307/// The class users live in.
308pub const USER_CLASS: &str = "_User";
309
310/// Narrow a `_Session` read to the caller's own sessions.
311///
312/// **Load-bearing, and it belongs here rather than in the router.** `_Session` rows carry no ACL,
313/// so the `_rperm $in [null, ...]` clause matches every one of them and this is the only thing
314/// standing between a client and every session token on the server.
315///
316/// Upstream narrows in the `_UnsafeRestQuery` constructor (`RestQuery.js:116-134`), not at a route
317/// handler, and that placement is the substance rather than an accident of where the code sits.
318/// The include path builds a real `RestQuery` (`RestQuery.js:1250-1258`), so an included
319/// `_Session` read is narrowed too. parse-rust had this at the router until 0.2.0, and
320/// `GET /classes/Leak?include=s` against a pointer to someone else's session returned that
321/// session's token. Every future consumer of this pipeline (LiveQuery matching, `afterFind`,
322/// aggregate) would have inherited the same gap.
323///
324/// Upstream wraps the client's whole where clause as `$and: [clientWhere, {user: <pointer>}]`
325/// rather than appending a sibling key, so that a client `$or` cannot widen it. A [`ParsedWhere`]
326/// is already a conjunction of clauses, so pushing one more conjunct is the same predicate.
327fn 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        // A caller with no user at all is refused outright rather than narrowed to nothing, which
338        // is upstream's order (`RestQuery.js:118-120`).
339        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    // `$and: [restWhere, {user}]` and not a pushed constraint (`RestQuery.js:121-131`). A client
355    // is free to send its own `user` constraint, and two equalities on one field spliced side by
356    // side collide in the transform rather than answering the query. The nesting is what lets the
357    // server's predicate and the client's coexist.
358    //
359    // The one departure: an empty `restWhere` is dropped rather than nested as `{}`. Upstream
360    // nests it, and `$and: [{}, ...]` is a valid but pointless branch.
361    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
370/// Steps 2 through 11 of the read ordering.
371fn 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        // Before everything, matching upstream's constructor-time position. Both of these are
386        // enforced here rather than at a route handler because upstream enforces them in the
387        // `RestQuery` constructor (`RestQuery.js:54`, `:116-134`), which the include path and
388        // `$relatedTo`'s authorization read both go through.
389        crate::class_security::enforce_class_security(
390            class_name,
391            master,
392            // The caller's `RestQuery.Method`, never the derived CLP operation. See [`ReadMethod`]
393            // for why the two cannot be collapsed.
394            method.as_str(),
395            ctx.options.error_detail,
396        )?;
397        narrow_sessions(&mut where_, class_name, ctx.scope, ctx.options.error_detail)?;
398
399        // 8 (computed early, because the denial below needs it). Master never reaches it.
400        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        // `denyProtectedFields`, which runs before the gate and against the unfiltered sort.
413        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        // 3. Sort validation. Unknown keys are dropped rather than refused, except `score`.
424        let order = validate_sort(schema, class_name, order)?;
425
426        // 4. The CLP gate.
427        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        // 11. Query validation, hoisted above the resolution steps because the keys it inspects
439        //     are the client's. Upstream runs it after pointer rewriting, on a query that by then
440        //     also carries the server's own `_rperm`/`_wperm` and has had `$relatedTo` deleted;
441        //     both of those are keys it would allow anyway, so checking the client's keys here is
442        //     the same predicate over a smaller set.
443        crate::query_parse::validate_query_keys(&where_, master)?;
444
445        // 5 and 6. `$relatedTo` and relation-field constraints, both join-table reads.
446        let mut query = resolve_where(ctx, class_name, schema, where_).await?;
447
448        // 7. Pointer permissions.
449        if !master {
450            match apply_pointer_permissions(schema, clp, op, &acl_group, &query)? {
451                PointerPermOutcome::Unconstrained => {}
452                PointerPermOutcome::Constrained(narrowed) => query = narrowed,
453                // 9. The deny check.
454                PointerPermOutcome::DenyAll => return Ok(ReadPlan::Denied),
455            }
456        }
457
458        // 10. The ACL clause.
459        //
460        // Upstream **overwrites** `_rperm`/`_wperm` at the query's top level rather than
461        // conjoining (`DatabaseController.js:78-90`), relying on the invariant that this runs
462        // last, after pointer rewriting, and on clients never being allowed to query those
463        // columns. A clause list conjoins instead, which is equivalent here and does not depend
464        // on the invariant holding.
465        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
481/// The read itself, without `include`.
482///
483/// Split from [`find`] because `include` runs one nested read per class per level and those must
484/// not themselves expand includes.
485fn 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                // 9. A denied `get` is `OBJECT_NOT_FOUND`; a denied `find` is empty.
499                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            // **The read path runs the same option the write path does**
514            // (`RestQuery.js:485-500`). Answering an empty result instead tells a client that
515            // cannot create classes that the class simply has no rows, which is a different
516            // statement from upstream's refusal and hides a misconfigured client behind a
517            // plausible-looking 200.
518            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        // 13.
532        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
551/// `keys` and `excludeKeys` folded into one positive projection.
552///
553/// `handleExcludeKeys` (`RestQuery.js:1039-1054`) subtracts from `keys` when there is one, and
554/// otherwise from the schema's field list, which is why this needs the schema.
555fn projection(schema: &ClassSchema, options: &FindOptions) -> Option<Vec<String>> {
556    // The four keys a projection can never drop (`AlwaysSelectedKeys`, `RestQuery.js:9`).
557    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            // `excludeKeys` never removes an always-selected key.
571            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
592/// Steps 5 and 6: turn a parsed where into a query, reading join tables where it has to.
593fn 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        // **Constraints on a `Relation` field are collected per field and resolved as a group**,
602        // because upstream's gate is a truthiness test on `query[key]`, the whole operator
603        // document, before it iterates that document's keys
604        // (`DatabaseController.js:1084-1112`). The parser has already split
605        // `{"$ne": false, "$in": [...]}` into two constraints on one field, so a per-constraint
606        // decision cannot see the sibling that satisfies the gate. Deciding one at a time returned
607        // every owner for a falsy `$ne`, where upstream returns none.
608        //
609        // Resolved after the loop rather than inside it, which also matches upstream:
610        // `reduceInRelation` runs over the finished query and `addInObjectIdsIds` folds its result
611        // into the constraints already there. Doing it inline meant an `objectId` constraint
612        // appearing later in the same where document was never intersected with the join result.
613        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                        // A `Relation` field has no column, so a constraint on one is the reverse
619                        // join read (`DatabaseController.js:1050-1143`).
620                        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                    // A caller who cannot read the owning object gets an empty `objectId $in`
636                    // rather than an error, so the relation is not a membership oracle.
637                    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                    // **This is a real difference, not an equivalent spelling.** Upstream's
651                    // `reduceInRelation` recurses into `$or` and `$and` but not `$nor`
652                    // (`DatabaseController.js:1054-1073`), so a relation constraint inside a
653                    // `$nor` reaches the adapter naming a column no document carries. It matches
654                    // nothing, and the `$nor` negates that into matching everything. parse-rust
655                    // resolves the join here instead, so the negated clause is an `objectId $in`
656                    // of the owning ids and the owners are excluded.
657                    //
658                    // The two agree only when the relation is empty. When it has members,
659                    // parse-rust returns the narrower answer: upstream's row set minus the rows
660                    // that are actually related. That is fail-closed, and it is the reason to
661                    // prefer it over reproducing a constraint that silently evaluates to a
662                    // tautology.
663                    //
664                    // Reasoned about from the upstream source, not measured against a running
665                    // parse-server. A differential over `$nor` plus `$relatedTo` would settle it.
666                    query.push(Clause::Nor(
667                        resolve_branches(ctx, class_name, schema, branches).await?,
668                    ));
669                }
670            }
671        }
672
673        // One group per relation field, each yielding as many reads as upstream builds queries:
674        // `{"$in": [a], "$nin": [b]}` is an inclusion and an exclusion, applied independently.
675        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
708/// `$relatedTo`, authorized against the owning class before the join table is read.
709async 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            // A read with the caller's own auth, so the owning class's CLP, the object's ACL and
737            // its pointer permissions all apply. Any denial or miss means "cannot read".
738            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
773/// Step 3: validate the sort and drop what the schema does not know.
774fn 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        // A sort key the schema does not carry is dropped rather than refused. `score` survives
793        // because it is a projected text-search rank rather than a column.
794        if schema.field(root).is_none() && field != "score" {
795            continue;
796        }
797        out.push((field.clone(), *direction));
798    }
799    Ok(out)
800}
801
802/// `^authData\.([a-zA-Z0-9_]+)\.id$`.
803fn 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
810/// Expand every `include` path, one query per target class per level.
811async 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            // One id is an equality, several are an `$in` (`RestQuery.js:1244-1249`), and the same
831            // count picks the method: `get` for one, `find` for several
832            // (`RestQuery.js:1250-1251`). The CLP operation does **not** follow it. Upstream pins
833            // that to `get` for every include regardless of how many ids it collected
834            // (`RestQuery.js:1259`), so a class granting `get` and denying `find` still serves an
835            // include of any size.
836            //
837            // What the method decides is `enforceRoleSecurity`: a multi-object include of
838            // `_Installation` is refused where a single-object one is allowed.
839            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            // The nested read is a full pipeline read with the caller's own scope, so the target
866            // class's CLP, ACL and protected fields all apply. Grafting the row in without this
867            // is the classic Parse data leak: the caller is authorized for the class holding the
868            // pointer, not for the class it points at.
869            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
883// ---------------------------------------------------------------------------------------------
884// Writes
885// ---------------------------------------------------------------------------------------------
886
887/// Create an object.
888pub 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    // The required-column check runs against the client body, before `ACL` is lowered into
900    // `_rperm`/`_wperm` and before relation ops are stripped. `_Role`'s ACL requirement is the
901    // load-bearing one: a role saved with no ACL is world-writable, so any client can add itself
902    // to it.
903    validate_required_columns(class_name, &as_plain_body(&body), false)?;
904
905    // `canAddField` precedes the operation gate on a write.
906    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    // 0.1.0 left creation ungated: `let _ = scope; // ACL does not gate creation; CLP would`.
925    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    // **Before the objectId is looked at, because `enforceClassExists` runs before every one of
937    // `validateObject`'s per-field checks** (`SchemaController.js:1288`), and the objectId type
938    // check below is one of them. With this after it, `{"objectId": 123}` against a class nobody
939    // has written answered `INCORRECT_TYPE` and left no `_SCHEMA` row, where parse-server answers
940    // the same error and leaves one. Measured against a running parse-server; Gate D asserts it.
941    //
942    // Still after the CLP gates above, which is a deliberate ordering difference from upstream and
943    // the one place this ordering is not a straight port. Upstream runs its `create` gate after
944    // `validateSchema`; here it runs first.
945    //
946    // **Do not reorder these to match upstream.** The rule this encodes is that a request
947    // parse-rust refuses leaves no durable state, and `_SCHEMA` is durable state on a database a
948    // parse-server node also reads. Without that rule the ordering looks arbitrary, which is why
949    // it is spelled out: for an otherwise-valid body the client-visible answer is the same denial
950    // either way, so nothing in the response shows the difference and no test that only reads
951    // responses catches a regression here. Recorded under the deliberate differences in
952    // `CHANGELOG.md`.
953    ensure_class_exists(ctx, class_name, class_exists).await?;
954
955    // Signup pre-generates an objectId so it can build the user's private ACL before the write.
956    // Honour one if it is already present rather than overwriting it, which would leave the ACL
957    // pointing at an id the row does not have.
958    //
959    // **Falsy, not absent, is the test upstream applies** (`RestWrite.js:429-431`, literally
960    // `if (!this.data.objectId)`). An empty string and a `null` are therefore replaced with a
961    // generated id rather than used, which is reachable at the default setting because
962    // `enforce_object_id_policy` refuses only *truthy* client ids there.
963    //
964    // A **truthy non-string** is the case that must not be replaced. `allowCustomObjectId` tests
965    // truthiness and nothing else, so `{"objectId": 123}` passes it, stays on the body, and is
966    // refused one step later by schema validation against the String type of the default column.
967    // Substituting a generated id here instead would create the row, report success, and hand the
968    // client an id it did not ask for, for a body upstream rejects.
969    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            // `enforceFieldExists` against `objectId`'s declared `String`
979            // (`SchemaController.js:1288-1318`). Answered here rather than left to
980            // `validate_write_fields` below, because by then the key has been overwritten.
981            return Err(match got {
982                Some(got) => schema_mismatch(class_name, "objectId", &FieldType::String, &got),
983                // No inferable type, which upstream skips entirely (`if (!expected) continue`).
984                // Unreachable for a truthy value, and refusing beats writing an unknown id.
985                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    // The schema delta is computed **before** the relation ops are stripped, because an
1004    // `AddRelation` is what reserves `Relation<Target>` for the field. Upstream reaches
1005    // `enforceFieldExists` through `validateSchema` while the op is still on the body, and
1006    // `collectRelationUpdates` only removes it on the way into the database controller. Stripping
1007    // first would leave a user-defined relation field with no `_SCHEMA` entry, which is invisible
1008    // until a `$relatedTo` against it returns nothing.
1009    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    // `ACL` is lowered after validation, because `_rperm` and `_wperm` are not fields and would
1016    // otherwise be validated as though a client had named them.
1017    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
1030/// Update one object by id.
1031pub 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    // A client cannot move an object or rewrite its creation time.
1044    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            // An update denied here resolves with no result upstream, which the caller's
1086            // `if (!result)` turns into `OBJECT_NOT_FOUND` (`DatabaseController.js:605-607`,
1087            // `:694-697`).
1088            PointerPermOutcome::DenyAll => return Err(object_not_found()),
1089        }
1090        if introduces_field {
1091            // The `addField` clause is conjoined on top of the `update` one
1092            // (`DatabaseController.js:590-603`).
1093            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    // Before the relation ops are stripped. See the note on the create path.
1112    //
1113    // **An update to a class nobody has written yet still creates the class row**, and then
1114    // matches nothing and answers `OBJECT_NOT_FOUND`. That reads like a bug and it is upstream's:
1115    // `validateSchema` is one step of the write chain (`RestWrite.js:127-128`) whichever path the
1116    // write is on, it calls `validateObject`, and that calls `enforceClassExists` before looking
1117    // at a single field (`SchemaController.js:1288`).
1118    //
1119    // Worth reproducing rather than "fixing", because the schema row is visible through
1120    // `GET /schemas` and through any parse-server node on the same database. 0.2.0 asserted the
1121    // opposite and got a body-dependent result instead: an empty update left nothing behind while
1122    // an update naming a new field created the class as a side effect of reserving that field.
1123    //
1124    // **And it happens before the fields are validated, not after.** A second review found the
1125    // first fix in the wrong order: `{"bad-key": 1}` is refused with `INVALID_KEY_NAME` by
1126    // `validate_write_fields`, and with the creation behind it the outcome was still
1127    // body-dependent, just along a different axis. `enforceClassExists` runs first upstream.
1128    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    // Only an update carrying a result-bearing operation needs the post-image read back.
1139    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
1162/// Move an `ACL` field out of the update and into the two storage columns.
1163///
1164/// UPSTREAM-QUIRK: `transformObjectACL` iterates whatever the `ACL` value happens to be
1165/// (`DatabaseController.js:93-110`), so an `{"__op":"Delete"}` on `ACL` produces two empty arrays
1166/// rather than unsetting the columns, which leaves the row readable and writable by master only.
1167fn 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        // `if (!ACL) return result` (`DatabaseController.js:94-96`): **every falsy ACL** is dropped
1174        // from the update rather than clearing the columns, so the row keeps the permissions it
1175        // had. Matching only `Null` here, which is what this did, let `false`, `0` and `""` fall
1176        // through to the unconditional write below and set both columns to empty arrays, which is
1177        // a master-only row the caller cannot undo.
1178        //
1179        // On `_User` that is worse than losing access to one row: `acl_is_explicitly_empty` reads
1180        // an empty ACL as a disabled account and refuses every later login, and
1181        // `force_owner_into_acl` does not defend against it because that only reinstates the owner
1182        // into an ACL that is an *object*. `{"ACL": {}}` is neutralised; `{"ACL": false}` was not.
1183        FieldWrite::Value(v) if !parse_rust_core::is_js_truthy(&v) => return,
1184        FieldWrite::Value(value) => value,
1185        // An op envelope is a truthy object upstream, so it falls through to the loop that reads
1186        // `read`/`write` off each entry and finds none. See the quirk note above.
1187        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
1201/// Delete one object by id.
1202pub 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            // A destroy denied here is `OBJECT_NOT_FOUND` (`DatabaseController.js:861-863`).
1232            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
1246/// Reserve the class and every new field **before** the row is written.
1247///
1248/// `create_class` is the create path's `enforceClassExists`.
1249///
1250/// This is the fix for the concurrent first-write race 0.1.0 shipped with. `reserve_field` is a
1251/// conditional upsert, so the loser of a race fails the condition rather than overwriting the
1252/// winner's type, and the outcome is an enum rather than an error code to sniff.
1253///
1254/// The class itself is reserved even when the write adds no field at all, which is the third
1255/// failure mode: a body of nothing but nulls infers no type, so without this it would insert a
1256/// row into a class with no `_SCHEMA` entry.
1257/// `enforceClassExists` (`SchemaController.js:979-1005`).
1258///
1259/// **Its position is the whole reason it is a separate function.** `validateObject` calls it
1260/// before it inspects a single field (`:1288`), so a write refused for a bad field name still
1261/// leaves the class behind. Folding it into `reserve_schema`, which is what this did until a
1262/// review, put it after `validate_write_fields` and made the schema side effect depend on whether
1263/// the body happened to be valid.
1264///
1265/// Only the default columns are written. The per-field reservations stay the atomic ones.
1266///
1267/// **An invalid class name is refused here, and it is refused as `INVALID_JSON` (107).** That
1268/// looks like the wrong code and it is upstream's, through a chain worth reading once:
1269/// `addClassIfNotExists` rejects with `INVALID_CLASS_NAME` and the detailed `Invalid classname:`
1270/// message, the `.catch` swallows it and reloads, the reload does not conjure the class, and the
1271/// terminal `.catch` replaces whatever happened with the fixed string
1272/// `schema class name does not revalidate` (`SchemaController.js:987-1004`). So the 103 a client
1273/// gets from `POST /schemas` and the 107 it gets from `POST /classes/1Bad` are the same underlying
1274/// refusal reported by two routes, and only the schema route sees the useful message.
1275///
1276/// Checking here rather than leaving it to `validate_write_fields` is what keeps the row from
1277/// being written: without it parse-rust answered 103 **and** left a `1BadClass` entry in `_SCHEMA`,
1278/// on a database parse-server also reads.
1279async 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
1296/// `validateClientClassCreation` (`RestWrite.js:196-219`).
1297///
1298/// Refuses a write that would bring a class into existence, unless the caller is privileged, the
1299/// option is on, or the class is one Parse defines itself. Upstream's option defaults to `false`
1300/// (`Options/Definitions.js:67-72`), so this is the ordinary configuration rather than a hardened
1301/// one, and a server without the check is more permissive than a stock parse-server.
1302///
1303/// **Ordering note.** Upstream reaches this before `validateSchema`, and so does this: it sits at
1304/// the top of `ensure_class_exists`, which is itself the first thing that would write a `_SCHEMA`
1305/// row. Both the create and the update path go through here, which is what upstream gets by
1306/// calling it from `RestWrite`'s shared chain rather than per route.
1307///
1308/// The exemption is by class name and not by caller, so a client can still sign up: `_User` and the
1309/// other system classes are always allowed to come into existence
1310/// (`SchemaController.js:165-176`).
1311fn validate_client_class_creation<S: StorageAdapter>(
1312    ctx: &Ctx<'_, S>,
1313    class_name: &str,
1314    maintenance_is_exempt: bool,
1315) -> Result<(), ParseError> {
1316    // **The two call sites do not agree about maintenance, and that is upstream's shape.** The
1317    // write path tests `!isMaster && !isMaintenance` (`RestWrite.js:200-202`); the read path tests
1318    // `!isMaster` alone (`RestQuery.js:486-489`), so a maintenance-key *read* of a class that does
1319    // not exist is refused there. Sharing one predicate silently gave maintenance the write path's
1320    // exemption on reads too.
1321    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    // `createSanitizedError` (`RestWrite.js:209-213`), so the detailed string is withheld at
1333    // upstream's default and the class name reaches the log instead.
1334    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            // No options: an ordinary write infers a type and never carries `required` or
1351            // `defaultValue`, which only the schema API can set.
1352            .reserve_field(class_name, field_name, field_type, None)
1353            .await?
1354        {
1355            AddFieldOutcome::Added | AddFieldOutcome::AlreadyPresentSameType => {}
1356            AddFieldOutcome::Conflict { existing } => {
1357                // The same `INCORRECT_TYPE` a plain type mismatch produces, because from the
1358                // client's side that is what happened: the field has a type and this write
1359                // disagrees with it.
1360                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    /// The default regime: `enableSanitizedErrorResponse` is `true` upstream, so every denial
1429    /// that goes through `createSanitizedError` says `Permission denied` and nothing else.
1430    fn opts() -> PermissionOptions {
1431        PermissionOptions::default()
1432    }
1433
1434    /// `enableSanitizedErrorResponse: false`. The detailed strings are contract under it, so the
1435    /// denial tests assert both regimes rather than picking one.
1436    fn disclosing_opts() -> PermissionOptions {
1437        PermissionOptions {
1438            error_detail: parse_rust_core::ErrorDetail::Disclosed,
1439            ..PermissionOptions::default()
1440        }
1441    }
1442
1443    // -----------------------------------------------------------------------------------------
1444    // The CLP gate
1445    // -----------------------------------------------------------------------------------------
1446
1447    /// 0.1.0 left creation ungated. This is the regression test for that line.
1448    #[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    /// The default-open rule, end to end. A class with no CLP block permits everything.
1486    #[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    /// The code is 101, not 119, and the class must not confirm its own existence.
1506    #[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    // -----------------------------------------------------------------------------------------
1555    // Pointer permissions
1556    // -----------------------------------------------------------------------------------------
1557
1558    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    /// The mitigation test the milestone names: **every** operation, anonymous caller, a class
1598    /// whose only permission is a pointer permission. Each must be empty or `OBJECT_NOT_FOUND`,
1599    /// never a full result set.
1600    #[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    // -----------------------------------------------------------------------------------------
1686    // Protected fields
1687    // -----------------------------------------------------------------------------------------
1688
1689    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    /// Without this a client binary-searches the protected value through `where`.
1730    #[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        // Nested inside a logical clause, and by its dotted root.
1765        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        // Master is exempt from the denial, not merely from the strip.
1796        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    // -----------------------------------------------------------------------------------------
1809    // ACL
1810    // -----------------------------------------------------------------------------------------
1811
1812    #[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    // -----------------------------------------------------------------------------------------
1881    // Atomic operations and schema reservation
1882    // -----------------------------------------------------------------------------------------
1883
1884    /// The 0.1.0 gap: the op decoder existed and the write path never called it.
1885    #[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    /// The field type is reserved atomically, before the row is written, so the loser of a race
1967    /// fails rather than overwriting the winner's type.
1968    #[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        // The snapshot does not know about `views`, so validation passes and the reservation is
1978        // what catches the conflict. That is the race, reproduced deterministically.
1979        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    // -----------------------------------------------------------------------------------------
2009    // Relations
2010    // -----------------------------------------------------------------------------------------
2011
2012    #[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        // The same membership added twice is one row.
2056        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        // And it reads back.
2073        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        // Removing the membership empties it.
2083        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    /// A caller who cannot read the owning object gets an empty result, not an error, so the
2099    /// relation is not a membership oracle.
2100    #[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        // The caller who can read the role sees the membership, which is what proves the empty
2135        // result above was the authorization and not a broken join read.
2136        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    /// A protected relation key on the owning class is a refusal rather than an empty result,
2148    /// because the key itself is the disclosure.
2149    #[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    // -----------------------------------------------------------------------------------------
2232    // include
2233    // -----------------------------------------------------------------------------------------
2234
2235    /// Ranked hazard 6: an included pointer is a full query against the target class with the
2236    /// caller's own auth. Grafting the row in without that is the classic Parse data leak.
2237    #[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    /// `allowClientClassCreation`, whose default is `false`. A server missing this check is more
2366    /// permissive than a stock parse-server, so the assertion that matters is that the *default*
2367    /// options refuse, not that the option works when set.
2368    #[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    /// The three exemptions, each for a different reason: the option, the master key, and the
2387    /// classes Parse defines itself. The last one is what keeps signup working with the option off.
2388    #[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    /// An `AddRelation` reserves `Relation<Target>` for the field, which is what makes a later
2417    /// `$relatedTo` against a user-defined class resolve at all. The op is stripped from the row
2418    /// write, but only after the schema has been reserved from it.
2419    #[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        // And the reverse read works, which is the observable consequence.
2453        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    /// A **falsy** `ACL` leaves the stored permissions alone rather than clearing them. Clearing
2504    /// them would lock every principal out of a row they still own, and on `_User` it disables the
2505    /// account outright: an empty ACL reads as "disabled" and refuses every later login.
2506    ///
2507    /// **The loop is the test.** This asserted `null` alone until a review, and the other three
2508    /// falsy values fell through to an unconditional write that set both columns to `[]`.
2509    /// Upstream's test is `if (!ACL)`, so all four behave the same there. Checking one value is
2510    /// exactly what let the other three through.
2511    #[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    /// An update to a class nobody has written yet answers `OBJECT_NOT_FOUND` **and leaves the
2553    /// class behind**, whatever the body contains.
2554    ///
2555    /// This test asserted the opposite until a review caught it, and the assertion was wrong in a
2556    /// way that hid a second problem: the outcome was body-dependent. An empty update really did
2557    /// leave nothing, while an update naming a new field created the class as a side effect of
2558    /// reserving the field. Upstream has one answer for both, because `enforceClassExists` runs
2559    /// from `validateSchema` before any field is looked at (`SchemaController.js:1288`,
2560    /// `RestWrite.js:127-128`).
2561    #[tokio::test]
2562    async fn an_update_to_a_missing_class_creates_the_class_and_then_finds_nothing() {
2563        // Including a body that is **rejected**, which is the case the first version of this fix
2564        // still got wrong: the class creation sat behind `validate_write_fields`, so a bad field
2565        // name skipped it. Upstream's `enforceClassExists` runs before any field is inspected, so
2566        // all three of these leave the class behind and only the error differs.
2567        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    /// An ACL written on a create round-trips through the two storage columns.
2591    #[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}