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::{default_acl_for_create, lower_acl, raise_acl, AclScope};
38use crate::clp::{
39    adds_field, apply_pointer_permissions, deny_protected_fields, filter_sensitive_data,
40    plan_protected_fields, validate_permission, PermissionOptions, PointerPermOutcome,
41    ProtectedFieldPlan, WriteAction,
42};
43use crate::include;
44use crate::query_parse::{ParsedClause, ParsedWhere};
45use crate::relations::{self, RelationConstraint};
46use crate::snapshot::SchemaSnapshot;
47use crate::write::{
48    as_plain_body, echo_response, echoed_keys, flatten_for_create, lower_update, WriteBody,
49};
50
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    // The class's CLP-declared default ACL (`RestWrite.js:378-395`).
956    //
957    // **0.2.0 accepted this setting, stored it, echoed it back from `GET /schemas` and never
958    // applied it**, so a class an operator had configured as private created world-readable rows:
959    // no `ACL` on the body means no `_rperm` or `_wperm` columns, and an absent `_rperm` is public.
960    // The configuration said one thing and the data did the other, which is worse than not
961    // supporting the feature.
962    //
963    // Three conditions, all upstream's and all easy to get subtly wrong:
964    //
965    // - **Create only.** Upstream guards on `!this.query`, so [`update`] has no counterpart to
966    //   this block. Stamping on update would silently revert an ACL a client changed on purpose.
967    // - **The body's `ACL` is tested for falsiness, not for presence** (`!this.data.ACL`), so a
968    //   client that sent `{"ACL": null}` gets the default, and only a truthy value suppresses it.
969    //   An `{"__op":"Delete"}` is an object and therefore truthy, so it suppresses it too.
970    // - **The public ACL is skipped**, by a key-order-sensitive comparison living in
971    //   [`parse_rust_core::ClassLevelPermissions::default_acl`].
972    //
973    // Placed after `ensure_class_exists` and after the required-column check, which is upstream's
974    // order: `validateSchema` runs both and precedes `setRequiredFieldsIfNeeded`. It matters for
975    // `_Role`, whose ACL is a required column: a role created with no ACL is refused rather than
976    // rescued by the class default.
977    //
978    // **The stamped ACL is returned in the create response**, which is a second thing the setting
979    // owes a client and not a cosmetic one: the caller has no other way to learn the permissions
980    // its object was given, and on a private class it cannot read the row back to find out.
981    // Upstream pushes `'ACL'` onto `fieldsChangedByTrigger` at `RestWrite.js:394` for exactly this
982    // reason. Measured against a parse-server at the pin: a create in such a class answers
983    // `{"objectId":…,"createdAt":…,"ACL":{"<callerId>":{"read":true,"write":true}}}`, and an
984    // anonymous create in the same class answers `"ACL":{}`. Both are reproduced, the empty object
985    // included.
986    let mut generated_acl = None;
987    if let Some(declared) = clp.and_then(|c| c.default_acl()) {
988        let suppressed = match body.get("ACL") {
989            Some(FieldWrite::Value(v)) => parse_rust_core::is_js_truthy(v),
990            Some(FieldWrite::Op(_)) => true,
991            None => false,
992        };
993        if !suppressed {
994            let acl = default_acl_for_create(declared, ctx.scope.user_id());
995            generated_acl = Some(acl.clone());
996            body.insert("ACL".to_string(), FieldWrite::Value(acl));
997        }
998    }
999
1000    // Signup pre-generates an objectId so it can build the user's private ACL before the write.
1001    // Honour one if it is already present rather than overwriting it, which would leave the ACL
1002    // pointing at an id the row does not have.
1003    //
1004    // **Falsy, not absent, is the test upstream applies** (`RestWrite.js:429-431`, literally
1005    // `if (!this.data.objectId)`). An empty string and a `null` are therefore replaced with a
1006    // generated id rather than used, which is reachable at the default setting because
1007    // `enforce_object_id_policy` refuses only *truthy* client ids there.
1008    //
1009    // A **truthy non-string** is the case that must not be replaced. `allowCustomObjectId` tests
1010    // truthiness and nothing else, so `{"objectId": 123}` passes it, stays on the body, and is
1011    // refused one step later by schema validation against the String type of the default column.
1012    // Substituting a generated id here instead would create the row, report success, and hand the
1013    // client an id it did not ask for, for a body upstream rejects.
1014    let object_id = match body.get("objectId") {
1015        None => new_object_id(),
1016        Some(FieldWrite::Value(v)) if !parse_rust_core::is_js_truthy(v) => new_object_id(),
1017        Some(FieldWrite::Value(ParseValue::String(id))) => id.clone(),
1018        Some(other) => {
1019            let got = match other {
1020                FieldWrite::Value(v) => infer_type(v),
1021                FieldWrite::Op(op) => infer_op_type(op)?,
1022            };
1023            // `enforceFieldExists` against `objectId`'s declared `String`
1024            // (`SchemaController.js:1288-1318`). Answered here rather than left to
1025            // `validate_write_fields` below, because by then the key has been overwritten.
1026            return Err(match got {
1027                Some(got) => schema_mismatch(class_name, "objectId", &FieldType::String, &got),
1028                // No inferable type, which upstream skips entirely (`if (!expected) continue`).
1029                //
1030                // **Reachable, and this is the arm that answers `{"__op":"Delete"}`.** An earlier
1031                // comment called it unreachable for a truthy value, which is wrong: a `Delete` is
1032                // truthy and has no inferred type. Upstream skips the check and answers 201, having
1033                // stored the row under a Mongo-generated `_id` while echoing the operation object
1034                // back as the `objectId`; parse-rust answers 107 instead. Recorded as a deliberate
1035                // difference and scoped for 0.3.0, which decides whether to keep it.
1036                None => ParseError::invalid_json("objectId is an invalid field name."),
1037            });
1038        }
1039    };
1040    let now = ParseDate::now();
1041    body.insert(
1042        "objectId".to_string(),
1043        FieldWrite::Value(ParseValue::String(object_id.clone())),
1044    );
1045    body.insert(
1046        "createdAt".to_string(),
1047        FieldWrite::Value(ParseValue::Date(now)),
1048    );
1049    body.insert(
1050        "updatedAt".to_string(),
1051        FieldWrite::Value(ParseValue::Date(now)),
1052    );
1053
1054    // The schema delta is computed **before** the relation ops are stripped, because an
1055    // `AddRelation` is what reserves `Relation<Target>` for the field. Upstream reaches
1056    // `enforceFieldExists` through `validateSchema` while the op is still on the body, and
1057    // `collectRelationUpdates` only removes it on the way into the database controller. Stripping
1058    // first would leave a user-defined relation field with no `_SCHEMA` entry, which is invisible
1059    // until a `$relatedTo` against it returns nothing.
1060    let delta = validate_write_fields(&schema, &body)?;
1061    reserve_schema(ctx, class_name, &schema, &delta.added).await?;
1062    apply(&mut schema, &delta);
1063
1064    let relation_updates = relations::collect_relation_updates(&mut body);
1065
1066    // **An `ACL` carrying an operation is an object upstream and disappears here.**
1067    // `flatten_for_create` removes a `Delete` op from the body entirely, so the `ACL` key is gone
1068    // by the time `lower_acl` runs, no permission columns are written, and an absent `_rperm` is
1069    // public. Upstream keeps `{"__op":"Delete"}` on `this.data.ACL`; `transformObjectACL` walks it,
1070    // finds no key carrying `read` or `write`, and writes two **empty** arrays, which is a
1071    // master-only row.
1072    //
1073    // Measured at the pin on an ordinary class: `{"ACL":{"__op":"Delete"}}` on a create answers
1074    // 201 on both servers, and an anonymous read of the object then answers **200 here and 404
1075    // upstream**. An empty object reproduces every op shape, because an op's keys are `__op`,
1076    // `objects` and `amount` and none of them carries a permission.
1077    //
1078    // `_User` never reaches this: `ensure_user_identity_and_acl` has already turned an op into the
1079    // owner-only ACL that upstream's `ACL[objectId] = ...` produces there.
1080    if matches!(body.get("ACL"), Some(FieldWrite::Op(_))) {
1081        body.insert(
1082            "ACL".to_string(),
1083            FieldWrite::Value(ParseValue::Object(ParseMap::new())),
1084        );
1085    }
1086
1087    // `ACL` is lowered after validation, because `_rperm` and `_wperm` are not fields and would
1088    // otherwise be validated as though a client had named them.
1089    let row = lower_acl(flatten_for_create(&body)?);
1090    ctx.storage.create(&schema, &row).await?;
1091
1092    relations::apply_relation_updates(ctx.storage, class_name, &object_id, &relation_updates)
1093        .await?;
1094
1095    // The operation echo, plus the ACL if this server generated one. `echo_response` reports only
1096    // what the *client* asked to echo, which is the right rule for the five result-bearing
1097    // operations and the wrong one here: the client did not ask, and upstream returns it anyway.
1098    let mut echoed = echo_response(&body, Some(&row));
1099    if let Some(acl) = generated_acl {
1100        echoed.insert("ACL".to_string(), acl);
1101    }
1102
1103    Ok(CreateResponse {
1104        object_id,
1105        created_at: now,
1106        echoed,
1107    })
1108}
1109
1110/// Update one object by id.
1111pub async fn update<S: StorageAdapter>(
1112    ctx: &Ctx<'_, S>,
1113    class_name: &str,
1114    object_id: &str,
1115    mut body: WriteBody,
1116) -> Result<UpdateResponse, ParseError> {
1117    let class_exists = ctx.snapshot.contains(class_name);
1118    let mut schema = ctx.snapshot.resolve_for_write(class_name);
1119    let clp = ctx.snapshot.clp(class_name);
1120    let acl_group = ctx.scope.acl_group();
1121    let master = ctx.scope.is_master();
1122
1123    // A client cannot move an object or rewrite its creation time.
1124    body.shift_remove("objectId");
1125    body.shift_remove("createdAt");
1126
1127    validate_required_columns(class_name, &as_plain_body(&body), true)?;
1128
1129    let introduces_field = adds_field(
1130        &schema,
1131        class_exists,
1132        body.keys().map(String::as_str),
1133        |key| matches!(body.get(key), Some(FieldWrite::Op(Op::Delete))),
1134    );
1135    if !master && introduces_field {
1136        validate_permission(
1137            clp,
1138            class_name,
1139            &acl_group,
1140            Operation::AddField,
1141            Some(WriteAction::Update),
1142            ctx.options.error_detail,
1143        )?;
1144    }
1145
1146    if !master {
1147        validate_permission(
1148            clp,
1149            class_name,
1150            &acl_group,
1151            Operation::Update,
1152            Some(WriteAction::Update),
1153            ctx.options.error_detail,
1154        )?;
1155    }
1156
1157    let mut query = Query::from_constraints(vec![Constraint::equal(
1158        "objectId",
1159        ParseValue::String(object_id.to_string()),
1160    )]);
1161    if !master {
1162        match apply_pointer_permissions(&schema, clp, Operation::Update, &acl_group, &query)? {
1163            PointerPermOutcome::Unconstrained => {}
1164            PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1165            // An update denied here resolves with no result upstream, which the caller's
1166            // `if (!result)` turns into `OBJECT_NOT_FOUND` (`DatabaseController.js:605-607`,
1167            // `:694-697`).
1168            PointerPermOutcome::DenyAll => return Err(object_not_found()),
1169        }
1170        if introduces_field {
1171            // The `addField` clause is conjoined on top of the `update` one
1172            // (`DatabaseController.js:590-603`).
1173            match apply_pointer_permissions(&schema, clp, Operation::AddField, &acl_group, &query)?
1174            {
1175                PointerPermOutcome::Unconstrained => {}
1176                PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1177                PointerPermOutcome::DenyAll => return Err(object_not_found()),
1178            }
1179        }
1180        if let Some(constraint) = ctx.scope.write_constraint() {
1181            query.push_constraint(constraint);
1182        }
1183    }
1184
1185    let updated_at = ParseDate::now();
1186    body.insert(
1187        "updatedAt".to_string(),
1188        FieldWrite::Value(ParseValue::Date(updated_at)),
1189    );
1190
1191    // Before the relation ops are stripped. See the note on the create path.
1192    //
1193    // **An update to a class nobody has written yet still creates the class row**, and then
1194    // matches nothing and answers `OBJECT_NOT_FOUND`. That reads like a bug and it is upstream's:
1195    // `validateSchema` is one step of the write chain (`RestWrite.js:127-128`) whichever path the
1196    // write is on, it calls `validateObject`, and that calls `enforceClassExists` before looking
1197    // at a single field (`SchemaController.js:1288`).
1198    //
1199    // Worth reproducing rather than "fixing", because the schema row is visible through
1200    // `GET /schemas` and through any parse-server node on the same database. 0.2.0 asserted the
1201    // opposite and got a body-dependent result instead: an empty update left nothing behind while
1202    // an update naming a new field created the class as a side effect of reserving that field.
1203    //
1204    // **And it happens before the fields are validated, not after.** A second review found the
1205    // first fix in the wrong order: `{"bad-key": 1}` is refused with `INVALID_KEY_NAME` by
1206    // `validate_write_fields`, and with the creation behind it the outcome was still
1207    // body-dependent, just along a different axis. `enforceClassExists` runs first upstream.
1208    ensure_class_exists(ctx, class_name, class_exists).await?;
1209    let delta = validate_write_fields(&schema, &body)?;
1210    reserve_schema(ctx, class_name, &schema, &delta.added).await?;
1211    apply(&mut schema, &delta);
1212
1213    let relation_updates = relations::collect_relation_updates(&mut body);
1214
1215    let mut update = lower_update(&body)?;
1216    lower_acl_into_update(&mut body, &mut update);
1217
1218    // Only an update carrying a result-bearing operation needs the post-image read back.
1219    let echoed = if echoed_keys(&body).is_empty() {
1220        let matched = ctx.storage.update(&schema, &query, &update).await?;
1221        if matched == 0 {
1222            return Err(object_not_found());
1223        }
1224        ParseMap::new()
1225    } else {
1226        let row = ctx
1227            .storage
1228            .update_one_returning(&schema, &query, &update)
1229            .await?;
1230        let Some(row) = row else {
1231            return Err(object_not_found());
1232        };
1233        echo_response(&body, Some(&row))
1234    };
1235
1236    relations::apply_relation_updates(ctx.storage, class_name, object_id, &relation_updates)
1237        .await?;
1238
1239    Ok(UpdateResponse { updated_at, echoed })
1240}
1241
1242/// Move an `ACL` field out of the update and into the two storage columns.
1243///
1244/// UPSTREAM-QUIRK: `transformObjectACL` iterates whatever the `ACL` value happens to be
1245/// (`DatabaseController.js:93-110`), so an `{"__op":"Delete"}` on `ACL` produces two empty arrays
1246/// rather than unsetting the columns, which leaves the row readable and writable by master only.
1247fn lower_acl_into_update(body: &mut WriteBody, update: &mut parse_rust_storage::Update) {
1248    let Some(write) = body.shift_remove("ACL") else {
1249        return;
1250    };
1251    update.shift_remove("ACL");
1252    let value = match write {
1253        // `if (!ACL) return result` (`DatabaseController.js:94-96`): **every falsy ACL** is dropped
1254        // from the update rather than clearing the columns, so the row keeps the permissions it
1255        // had. Matching only `Null` here, which is what this did, let `false`, `0` and `""` fall
1256        // through to the unconditional write below and set both columns to empty arrays, which is
1257        // a master-only row the caller cannot undo.
1258        //
1259        // On `_User` that is worse than losing access to one row: `acl_is_explicitly_empty` reads
1260        // an empty ACL as a disabled account and refuses every later login, and
1261        // `force_owner_into_acl` does not defend against it because that only reinstates the owner
1262        // into an ACL that is an *object*. `{"ACL": {}}` is neutralised; `{"ACL": false}` was not.
1263        FieldWrite::Value(v) if !parse_rust_core::is_js_truthy(&v) => return,
1264        FieldWrite::Value(value) => value,
1265        // An op envelope is a truthy object upstream, so it falls through to the loop that reads
1266        // `read`/`write` off each entry and finds none. See the quirk note above.
1267        FieldWrite::Op(_) => ParseValue::Object(ParseMap::new()),
1268    };
1269    let mut carrier = ParseMap::new();
1270    carrier.insert("ACL".to_string(), value);
1271    let lowered = lower_acl(carrier);
1272    for key in ["_rperm", "_wperm"] {
1273        let value = lowered
1274            .get(key)
1275            .cloned()
1276            .unwrap_or(ParseValue::Array(Vec::new()));
1277        update.insert(key.to_string(), UpdateValue::Set(value));
1278    }
1279}
1280
1281/// Delete one object by id.
1282pub async fn delete<S: StorageAdapter>(
1283    ctx: &Ctx<'_, S>,
1284    class_name: &str,
1285    object_id: &str,
1286) -> Result<(), ParseError> {
1287    let schema = ctx.snapshot.get_or_default(class_name);
1288    let clp = ctx.snapshot.clp(class_name);
1289    let acl_group = ctx.scope.acl_group();
1290    let master = ctx.scope.is_master();
1291
1292    if !master {
1293        validate_permission(
1294            clp,
1295            class_name,
1296            &acl_group,
1297            Operation::Delete,
1298            None,
1299            ctx.options.error_detail,
1300        )?;
1301    }
1302
1303    let mut query = Query::from_constraints(vec![Constraint::equal(
1304        "objectId",
1305        ParseValue::String(object_id.to_string()),
1306    )]);
1307    if !master {
1308        match apply_pointer_permissions(&schema, clp, Operation::Delete, &acl_group, &query)? {
1309            PointerPermOutcome::Unconstrained => {}
1310            PointerPermOutcome::Constrained(narrowed) => query = narrowed,
1311            // A destroy denied here is `OBJECT_NOT_FOUND` (`DatabaseController.js:861-863`).
1312            PointerPermOutcome::DenyAll => return Err(object_not_found()),
1313        }
1314        if let Some(constraint) = ctx.scope.write_constraint() {
1315            query.push_constraint(constraint);
1316        }
1317    }
1318
1319    let deleted = ctx.storage.delete(&schema, &query).await?;
1320    if deleted == 0 {
1321        return Err(object_not_found());
1322    }
1323    Ok(())
1324}
1325
1326/// Reserve the class and every new field **before** the row is written.
1327///
1328/// `create_class` is the create path's `enforceClassExists`.
1329///
1330/// This is the fix for the concurrent first-write race 0.1.0 shipped with. `reserve_field` is a
1331/// conditional upsert, so the loser of a race fails the condition rather than overwriting the
1332/// winner's type, and the outcome is an enum rather than an error code to sniff.
1333///
1334/// The class itself is reserved even when the write adds no field at all, which is the third
1335/// failure mode: a body of nothing but nulls infers no type, so without this it would insert a
1336/// row into a class with no `_SCHEMA` entry.
1337/// `enforceClassExists` (`SchemaController.js:979-1005`).
1338///
1339/// **Its position is the whole reason it is a separate function.** `validateObject` calls it
1340/// before it inspects a single field (`:1288`), so a write refused for a bad field name still
1341/// leaves the class behind. Folding it into `reserve_schema`, which is what this did until a
1342/// review, put it after `validate_write_fields` and made the schema side effect depend on whether
1343/// the body happened to be valid.
1344///
1345/// Only the default columns are written. The per-field reservations stay the atomic ones.
1346///
1347/// **An invalid class name is refused here, and it is refused as `INVALID_JSON` (107).** That
1348/// looks like the wrong code and it is upstream's, through a chain worth reading once:
1349/// `addClassIfNotExists` rejects with `INVALID_CLASS_NAME` and the detailed `Invalid classname:`
1350/// message, the `.catch` swallows it and reloads, the reload does not conjure the class, and the
1351/// terminal `.catch` replaces whatever happened with the fixed string
1352/// `schema class name does not revalidate` (`SchemaController.js:987-1004`). So the 103 a client
1353/// gets from `POST /schemas` and the 107 it gets from `POST /classes/1Bad` are the same underlying
1354/// refusal reported by two routes, and only the schema route sees the useful message.
1355///
1356/// Checking here rather than leaving it to `validate_write_fields` is what keeps the row from
1357/// being written: without it parse-rust answered 103 **and** left a `1BadClass` entry in `_SCHEMA`,
1358/// on a database parse-server also reads.
1359async fn ensure_class_exists<S: StorageAdapter>(
1360    ctx: &Ctx<'_, S>,
1361    class_name: &str,
1362    class_exists: bool,
1363) -> Result<(), ParseError> {
1364    if class_exists {
1365        return Ok(());
1366    }
1367    validate_client_class_creation(ctx, class_name, true)?;
1368    if !parse_rust_schema::class_name_is_valid(class_name) {
1369        return Err(ParseError::invalid_json(
1370            "schema class name does not revalidate",
1371        ));
1372    }
1373    ctx.storage.upsert_schema(&default_schema(class_name)).await
1374}
1375
1376/// `validateClientClassCreation` (`RestWrite.js:196-219`).
1377///
1378/// Refuses a write that would bring a class into existence, unless the caller is privileged, the
1379/// option is on, or the class is one Parse defines itself. Upstream's option defaults to `false`
1380/// (`Options/Definitions.js:67-72`), so this is the ordinary configuration rather than a hardened
1381/// one, and a server without the check is more permissive than a stock parse-server.
1382///
1383/// **Ordering note.** Upstream reaches this before `validateSchema`, and so does this: it sits at
1384/// the top of `ensure_class_exists`, which is itself the first thing that would write a `_SCHEMA`
1385/// row. Both the create and the update path go through here, which is what upstream gets by
1386/// calling it from `RestWrite`'s shared chain rather than per route.
1387///
1388/// The exemption is by class name and not by caller, so a client can still sign up: `_User` and the
1389/// other system classes are always allowed to come into existence
1390/// (`SchemaController.js:165-176`).
1391fn validate_client_class_creation<S: StorageAdapter>(
1392    ctx: &Ctx<'_, S>,
1393    class_name: &str,
1394    maintenance_is_exempt: bool,
1395) -> Result<(), ParseError> {
1396    // **The two call sites do not agree about maintenance, and that is upstream's shape.** The
1397    // write path tests `!isMaster && !isMaintenance` (`RestWrite.js:200-202`); the read path tests
1398    // `!isMaster` alone (`RestQuery.js:486-489`), so a maintenance-key *read* of a class that does
1399    // not exist is refused there. Sharing one predicate silently gave maintenance the write path's
1400    // exemption on reads too.
1401    let privileged = if maintenance_is_exempt {
1402        ctx.scope.is_master()
1403    } else {
1404        ctx.scope.is_master() && !ctx.is_maintenance
1405    };
1406    if ctx.options.allow_client_class_creation
1407        || privileged
1408        || parse_rust_schema::SYSTEM_CLASSES.contains(&class_name)
1409    {
1410        return Ok(());
1411    }
1412    // `createSanitizedError` (`RestWrite.js:209-213`), so the detailed string is withheld at
1413    // upstream's default and the class name reaches the log instead.
1414    Err(ParseError::permission_denied(
1415        ErrorCode::OperationForbidden,
1416        format!("This user is not allowed to access non-existent class: {class_name}"),
1417        ctx.options.error_detail,
1418    ))
1419}
1420
1421async fn reserve_schema<S: StorageAdapter>(
1422    ctx: &Ctx<'_, S>,
1423    class_name: &str,
1424    schema: &ClassSchema,
1425    added: &[(String, FieldType)],
1426) -> Result<(), ParseError> {
1427    for (field_name, field_type) in added {
1428        match ctx
1429            .storage
1430            // No options: an ordinary write infers a type and never carries `required` or
1431            // `defaultValue`, which only the schema API can set.
1432            .reserve_field(class_name, field_name, field_type, None)
1433            .await?
1434        {
1435            AddFieldOutcome::Added | AddFieldOutcome::AlreadyPresentSameType => {}
1436            AddFieldOutcome::Conflict { existing } => {
1437                // The same `INCORRECT_TYPE` a plain type mismatch produces, because from the
1438                // client's side that is what happened: the field has a type and this write
1439                // disagrees with it.
1440                return Err(schema_mismatch(
1441                    &schema.class_name,
1442                    field_name,
1443                    &existing,
1444                    field_type,
1445                ));
1446            }
1447        }
1448    }
1449    Ok(())
1450}
1451
1452#[cfg(test)]
1453mod tests {
1454    use super::*;
1455    use crate::query_parse::parse_where;
1456    use crate::testing::FakeStorage;
1457    use crate::write::decode_write_body;
1458    use parse_rust_core::op::OpPath;
1459    use parse_rust_core::ClassLevelPermissions;
1460
1461    fn clp(json: &str) -> ClassLevelPermissions {
1462        let value = parse_rust_core::classify(
1463            serde_json::from_str(json).expect("test literal must be valid JSON"),
1464        )
1465        .expect("classify");
1466        match value {
1467            ParseValue::Object(m) => ClassLevelPermissions::from_map(m),
1468            _ => panic!("expected an object"),
1469        }
1470    }
1471
1472    fn where_(json: &str) -> ParsedWhere {
1473        parse_where(&serde_json::from_str(json).expect("test literal")).expect("parse")
1474    }
1475
1476    fn body(json: &str, path: OpPath) -> WriteBody {
1477        decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
1478    }
1479
1480    fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
1481        let mut m = ParseMap::new();
1482        for (k, v) in pairs {
1483            m.insert(k.to_string(), v);
1484        }
1485        m
1486    }
1487
1488    fn strings(values: &[&str]) -> ParseValue {
1489        ParseValue::Array(
1490            values
1491                .iter()
1492                .map(|v| ParseValue::String((*v).to_string()))
1493                .collect(),
1494        )
1495    }
1496
1497    fn pointer(class: &str, id: &str) -> ParseValue {
1498        ParseValue::Pointer {
1499            class_name: class.to_string(),
1500            object_id: id.to_string(),
1501        }
1502    }
1503
1504    async fn snapshot(storage: &FakeStorage) -> SchemaSnapshot {
1505        SchemaSnapshot::load(storage).await.expect("snapshot")
1506    }
1507
1508    /// The default regime: `enableSanitizedErrorResponse` is `true` upstream, so every denial
1509    /// that goes through `createSanitizedError` says `Permission denied` and nothing else.
1510    fn opts() -> PermissionOptions {
1511        PermissionOptions::default()
1512    }
1513
1514    /// `enableSanitizedErrorResponse: false`. The detailed strings are contract under it, so the
1515    /// denial tests assert both regimes rather than picking one.
1516    fn disclosing_opts() -> PermissionOptions {
1517        PermissionOptions {
1518            error_detail: parse_rust_core::ErrorDetail::Disclosed,
1519            ..PermissionOptions::default()
1520        }
1521    }
1522
1523    // -----------------------------------------------------------------------------------------
1524    // The CLP gate
1525    // -----------------------------------------------------------------------------------------
1526
1527    /// 0.1.0 left creation ungated. This is the regression test for that line.
1528    #[tokio::test]
1529    async fn create_runs_the_clp_gate() {
1530        let storage = FakeStorage::new().with_schema(
1531            default_schema("Post").with_clp(clp(r#"{"create":{"role:Writers":true}}"#)),
1532        );
1533        let snap = snapshot(&storage).await;
1534        let options = opts();
1535
1536        let anon = AclScope::Anonymous;
1537        let ctx = Ctx::new(&storage, &snap, &anon, &options);
1538        let e = create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1539            .await
1540            .unwrap_err();
1541        assert_eq!(e.code, ErrorCode::OperationForbidden);
1542        assert_eq!(e.message, "Permission denied");
1543        assert!(storage.rows("Post").is_empty(), "nothing was written");
1544
1545        let disclosing = disclosing_opts();
1546        let ctx = Ctx::new(&storage, &snap, &anon, &disclosing);
1547        assert_eq!(
1548            create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1549                .await
1550                .unwrap_err()
1551                .message,
1552            "Permission denied for action create on class Post."
1553        );
1554        assert!(storage.rows("Post").is_empty(), "nothing was written");
1555
1556        let writer = AclScope::user("u1", vec!["Writers".into()]).expect("scope");
1557        let ctx = Ctx::new(&storage, &snap, &writer, &options);
1558        assert!(
1559            create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1560                .await
1561                .is_ok()
1562        );
1563    }
1564
1565    /// The default-open rule, end to end. A class with no CLP block permits everything.
1566    #[tokio::test]
1567    async fn a_class_with_no_clp_is_unrestricted() {
1568        let storage = FakeStorage::new().with_schema(default_schema("Post"));
1569        let snap = snapshot(&storage).await;
1570        let options = opts();
1571        let anon = AclScope::Anonymous;
1572        let ctx = Ctx::new(&storage, &snap, &anon, &options);
1573        assert!(
1574            create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1575                .await
1576                .is_ok()
1577        );
1578        assert!(
1579            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1580                .await
1581                .is_ok()
1582        );
1583    }
1584
1585    /// The code is 101, not 119, and the class must not confirm its own existence.
1586    #[tokio::test]
1587    async fn requires_authentication_denies_a_read_with_object_not_found() {
1588        let storage = FakeStorage::new()
1589            .with_schema(
1590                default_schema("Post").with_clp(clp(r#"{"find":{"requiresAuthentication":true}}"#)),
1591            )
1592            .with_row(
1593                "Post",
1594                row(vec![("objectId", ParseValue::String("p1".into()))]),
1595            );
1596        let snap = snapshot(&storage).await;
1597        let options = opts();
1598
1599        let anon = AclScope::Anonymous;
1600        let ctx = Ctx::new(&storage, &snap, &anon, &options);
1601        let e = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1602            .await
1603            .unwrap_err();
1604        assert_eq!(e.code, ErrorCode::ObjectNotFound);
1605        assert_eq!(e.message, "Permission denied");
1606
1607        let disclosing = disclosing_opts();
1608        let disclosed = Ctx::new(&storage, &snap, &anon, &disclosing);
1609        let e = find(
1610            &disclosed,
1611            "Post",
1612            ParsedWhere::default(),
1613            FindOptions::default(),
1614        )
1615        .await
1616        .unwrap_err();
1617        assert_eq!(e.code, ErrorCode::ObjectNotFound);
1618        assert_eq!(
1619            e.message,
1620            "Permission denied, user needs to be authenticated."
1621        );
1622
1623        let user = AclScope::user("u1", vec![]).expect("scope");
1624        let ctx = Ctx::new(&storage, &snap, &user, &options);
1625        assert_eq!(
1626            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1627                .await
1628                .expect("allowed")
1629                .len(),
1630            1
1631        );
1632    }
1633
1634    // -----------------------------------------------------------------------------------------
1635    // Pointer permissions
1636    // -----------------------------------------------------------------------------------------
1637
1638    fn pointer_perm_storage() -> FakeStorage {
1639        let clp_json = r#"{
1640            "find":{"pointerFields":["owner"]},
1641            "get":{"pointerFields":["owner"]},
1642            "count":{"pointerFields":["owner"]},
1643            "update":{"pointerFields":["owner"]},
1644            "delete":{"pointerFields":["owner"]},
1645            "create":{"*":true}
1646        }"#;
1647        FakeStorage::new()
1648            .with_schema(
1649                default_schema("Post")
1650                    .with_field(
1651                        "owner",
1652                        FieldType::Pointer {
1653                            target_class: "_User".into(),
1654                        },
1655                    )
1656                    .with_field("title", FieldType::String)
1657                    .with_clp(clp(clp_json)),
1658            )
1659            .with_row(
1660                "Post",
1661                row(vec![
1662                    ("objectId", ParseValue::String("p1".into())),
1663                    ("owner", pointer("_User", "u1")),
1664                    ("title", ParseValue::String("mine".into())),
1665                ]),
1666            )
1667            .with_row(
1668                "Post",
1669                row(vec![
1670                    ("objectId", ParseValue::String("p2".into())),
1671                    ("owner", pointer("_User", "u2")),
1672                    ("title", ParseValue::String("theirs".into())),
1673                ]),
1674            )
1675    }
1676
1677    /// The mitigation test the milestone names: **every** operation, anonymous caller, a class
1678    /// whose only permission is a pointer permission. Each must be empty or `OBJECT_NOT_FOUND`,
1679    /// never a full result set.
1680    #[tokio::test]
1681    async fn an_anonymous_caller_gets_nothing_from_a_pointer_permission_class() {
1682        let storage = pointer_perm_storage();
1683        let snap = snapshot(&storage).await;
1684        let options = opts();
1685        let anon = AclScope::Anonymous;
1686        let ctx = Ctx::new(&storage, &snap, &anon, &options);
1687
1688        assert!(
1689            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1690                .await
1691                .expect("find resolves empty rather than erroring")
1692                .is_empty()
1693        );
1694        assert_eq!(
1695            get(&ctx, "Post", "p1", FindOptions::default())
1696                .await
1697                .unwrap_err()
1698                .code,
1699            ErrorCode::ObjectNotFound
1700        );
1701        assert_eq!(
1702            count(&ctx, "Post", ParsedWhere::default())
1703                .await
1704                .expect("count resolves"),
1705            0
1706        );
1707        assert_eq!(
1708            update(&ctx, "Post", "p1", body(r#"{"title":"x"}"#, OpPath::Update))
1709                .await
1710                .unwrap_err()
1711                .code,
1712            ErrorCode::ObjectNotFound
1713        );
1714        assert_eq!(
1715            delete(&ctx, "Post", "p1").await.unwrap_err().code,
1716            ErrorCode::ObjectNotFound
1717        );
1718        assert_eq!(storage.rows("Post").len(), 2, "nothing was deleted");
1719    }
1720
1721    #[tokio::test]
1722    async fn a_pointer_permission_narrows_a_user_to_their_own_rows() {
1723        let storage = pointer_perm_storage();
1724        let snap = snapshot(&storage).await;
1725        let options = opts();
1726        let u1 = AclScope::user("u1", vec![]).expect("scope");
1727        let ctx = Ctx::new(&storage, &snap, &u1, &options);
1728
1729        let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1730            .await
1731            .expect("find");
1732        assert_eq!(results.len(), 1);
1733        assert!(matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "p1"));
1734
1735        assert!(get(&ctx, "Post", "p1", FindOptions::default())
1736            .await
1737            .is_ok());
1738        assert_eq!(
1739            get(&ctx, "Post", "p2", FindOptions::default())
1740                .await
1741                .unwrap_err()
1742                .code,
1743            ErrorCode::ObjectNotFound
1744        );
1745        assert_eq!(
1746            count(&ctx, "Post", ParsedWhere::default())
1747                .await
1748                .expect("count"),
1749            1
1750        );
1751        assert!(
1752            update(&ctx, "Post", "p1", body(r#"{"title":"x"}"#, OpPath::Update))
1753                .await
1754                .is_ok()
1755        );
1756        assert_eq!(
1757            update(&ctx, "Post", "p2", body(r#"{"title":"x"}"#, OpPath::Update))
1758                .await
1759                .unwrap_err()
1760                .code,
1761            ErrorCode::ObjectNotFound
1762        );
1763    }
1764
1765    // -----------------------------------------------------------------------------------------
1766    // Protected fields
1767    // -----------------------------------------------------------------------------------------
1768
1769    fn protected_storage() -> FakeStorage {
1770        FakeStorage::new()
1771            .with_schema(
1772                default_schema("Post")
1773                    .with_field("secret", FieldType::String)
1774                    .with_field("title", FieldType::String)
1775                    .with_clp(clp(r#"{"protectedFields":{"*":["secret"]}}"#)),
1776            )
1777            .with_row(
1778                "Post",
1779                row(vec![
1780                    ("objectId", ParseValue::String("p1".into())),
1781                    ("title", ParseValue::String("t".into())),
1782                    ("secret", ParseValue::String("s".into())),
1783                ]),
1784            )
1785    }
1786
1787    #[tokio::test]
1788    async fn a_protected_field_is_absent_from_a_read_and_present_for_master() {
1789        let storage = protected_storage();
1790        let snap = snapshot(&storage).await;
1791        let options = opts();
1792
1793        let anon = AclScope::Anonymous;
1794        let ctx = Ctx::new(&storage, &snap, &anon, &options);
1795        let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1796            .await
1797            .expect("find");
1798        assert!(results[0].get("secret").is_none());
1799        assert!(results[0].get("title").is_some());
1800
1801        let master = AclScope::Unrestricted;
1802        let ctx = Ctx::new(&storage, &snap, &master, &options);
1803        let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1804            .await
1805            .expect("find");
1806        assert!(results[0].get("secret").is_some());
1807    }
1808
1809    /// Without this a client binary-searches the protected value through `where`.
1810    #[tokio::test]
1811    async fn querying_or_ordering_by_a_protected_field_is_forbidden() {
1812        let storage = protected_storage();
1813        let snap = snapshot(&storage).await;
1814        let options = opts();
1815        let anon = AclScope::Anonymous;
1816        let ctx = Ctx::new(&storage, &snap, &anon, &options);
1817
1818        let e = find(
1819            &ctx,
1820            "Post",
1821            where_(r#"{"secret":"s"}"#),
1822            FindOptions::default(),
1823        )
1824        .await
1825        .unwrap_err();
1826        assert_eq!(e.code, ErrorCode::OperationForbidden);
1827        assert_eq!(e.message, "Permission denied");
1828
1829        let disclosing = disclosing_opts();
1830        let disclosed = Ctx::new(&storage, &snap, &anon, &disclosing);
1831        assert_eq!(
1832            find(
1833                &disclosed,
1834                "Post",
1835                where_(r#"{"secret":"s"}"#),
1836                FindOptions::default()
1837            )
1838            .await
1839            .unwrap_err()
1840            .message,
1841            "This user is not allowed to query secret on class Post"
1842        );
1843
1844        // Nested inside a logical clause, and by its dotted root.
1845        assert_eq!(
1846            find(
1847                &ctx,
1848                "Post",
1849                where_(r#"{"$or":[{"secret.a":"s"}]}"#),
1850                FindOptions::default()
1851            )
1852            .await
1853            .unwrap_err()
1854            .code,
1855            ErrorCode::OperationForbidden
1856        );
1857
1858        let sorted = FindOptions {
1859            order: vec![("secret".to_string(), SortDirection::Ascending)],
1860            ..Default::default()
1861        };
1862        let e = find(&ctx, "Post", ParsedWhere::default(), sorted.clone())
1863            .await
1864            .unwrap_err();
1865        assert_eq!(e.code, ErrorCode::OperationForbidden);
1866        assert_eq!(e.message, "Permission denied");
1867        assert_eq!(
1868            find(&disclosed, "Post", ParsedWhere::default(), sorted)
1869                .await
1870                .unwrap_err()
1871                .message,
1872            "This user is not allowed to sort by secret on class Post"
1873        );
1874
1875        // Master is exempt from the denial, not merely from the strip.
1876        let master = AclScope::Unrestricted;
1877        let ctx = Ctx::new(&storage, &snap, &master, &options);
1878        assert!(find(
1879            &ctx,
1880            "Post",
1881            where_(r#"{"secret":"s"}"#),
1882            FindOptions::default()
1883        )
1884        .await
1885        .is_ok());
1886    }
1887
1888    // -----------------------------------------------------------------------------------------
1889    // ACL
1890    // -----------------------------------------------------------------------------------------
1891
1892    #[tokio::test]
1893    async fn an_acl_hides_a_row_from_everyone_but_its_principals() {
1894        let storage = FakeStorage::new()
1895            .with_schema(default_schema("Post"))
1896            .with_row(
1897                "Post",
1898                row(vec![
1899                    ("objectId", ParseValue::String("private".into())),
1900                    ("_rperm", strings(&["u1", "role:Admins"])),
1901                    ("_wperm", strings(&["u1"])),
1902                ]),
1903            )
1904            .with_row(
1905                "Post",
1906                row(vec![("objectId", ParseValue::String("public".into()))]),
1907            );
1908        let snap = snapshot(&storage).await;
1909        let options = opts();
1910
1911        let owner = AclScope::user("u1", vec![]).expect("scope");
1912        let ctx = Ctx::new(&storage, &snap, &owner, &options);
1913        assert_eq!(
1914            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1915                .await
1916                .expect("find")
1917                .len(),
1918            2,
1919            "assert first that the owner can see its own row"
1920        );
1921
1922        let admin = AclScope::user("u2", vec!["Admins".into()]).expect("scope");
1923        let ctx = Ctx::new(&storage, &snap, &admin, &options);
1924        assert_eq!(
1925            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1926                .await
1927                .expect("find")
1928                .len(),
1929            2,
1930            "a role: entry in _rperm must match a member"
1931        );
1932
1933        let stranger = AclScope::user("u3", vec![]).expect("scope");
1934        let ctx = Ctx::new(&storage, &snap, &stranger, &options);
1935        let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1936            .await
1937            .expect("find");
1938        assert_eq!(results.len(), 1);
1939        assert!(
1940            matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "public")
1941        );
1942        assert_eq!(
1943            update(
1944                &ctx,
1945                "Post",
1946                "private",
1947                body(r#"{"title":"x"}"#, OpPath::Update)
1948            )
1949            .await
1950            .unwrap_err()
1951            .code,
1952            ErrorCode::ObjectNotFound
1953        );
1954        assert_eq!(
1955            delete(&ctx, "Post", "private").await.unwrap_err().code,
1956            ErrorCode::ObjectNotFound
1957        );
1958    }
1959
1960    // -----------------------------------------------------------------------------------------
1961    // The CLP-declared default ACL
1962    //
1963    // Every assertion below is a read or a write rather than an inspection of `_rperm`, because
1964    // the failure being guarded is that no permission columns are written at all, and a test that
1965    // looks at a column and finds it missing has to decide what missing means. A request does not.
1966    // -----------------------------------------------------------------------------------------
1967
1968    /// A class declared private, an object created by user A, and the two halves that a naive
1969    /// test would only get half of: user B is shut out, **and user A is not**. An implementation
1970    /// that wrote an empty ACL, or that stored the literal string `currentUser` as a principal,
1971    /// would deny B and pass the first half while locking out the owner.
1972    #[tokio::test]
1973    async fn a_declared_default_acl_isolates_the_creator_without_locking_them_out() {
1974        let storage = FakeStorage::new().with_schema(
1975            default_schema("Post")
1976                .with_clp(clp(r#"{"ACL":{"currentUser":{"read":true,"write":true}}}"#)),
1977        );
1978        let snap = snapshot(&storage).await;
1979        let options = opts();
1980
1981        let a = AclScope::user("userA", vec![]).expect("scope");
1982        let ctx = Ctx::new(&storage, &snap, &a, &options);
1983        let created = create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
1984            .await
1985            .expect("create");
1986
1987        assert_eq!(
1988            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
1989                .await
1990                .expect("find")
1991                .len(),
1992            1,
1993            "the creator must still be able to read its own object"
1994        );
1995        assert!(
1996            update(
1997                &ctx,
1998                "Post",
1999                &created.object_id,
2000                body(r#"{"title":"y"}"#, OpPath::Update)
2001            )
2002            .await
2003            .is_ok(),
2004            "and to write it: _wperm is a separate column and can be wrong on its own"
2005        );
2006
2007        let b = AclScope::user("userB", vec![]).expect("scope");
2008        let ctx = Ctx::new(&storage, &snap, &b, &options);
2009        assert!(
2010            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
2011                .await
2012                .expect("find")
2013                .is_empty(),
2014            "0.2.0 returned this object to every caller"
2015        );
2016        assert_eq!(
2017            update(
2018                &ctx,
2019                "Post",
2020                &created.object_id,
2021                body(r#"{"title":"z"}"#, OpPath::Update)
2022            )
2023            .await
2024            .unwrap_err()
2025            .code,
2026            ErrorCode::ObjectNotFound
2027        );
2028    }
2029
2030    /// The control. Without it the test above passes against a pipeline that lost the ability to
2031    /// read anything at all.
2032    #[tokio::test]
2033    async fn a_class_with_no_declared_acl_still_creates_public_rows() {
2034        let storage = FakeStorage::new().with_schema(default_schema("Post"));
2035        let snap = snapshot(&storage).await;
2036        let options = opts();
2037
2038        let a = AclScope::user("userA", vec![]).expect("scope");
2039        let ctx = Ctx::new(&storage, &snap, &a, &options);
2040        create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
2041            .await
2042            .expect("create");
2043
2044        let b = AclScope::user("userB", vec![]).expect("scope");
2045        let ctx = Ctx::new(&storage, &snap, &b, &options);
2046        assert_eq!(
2047            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
2048                .await
2049                .expect("find")
2050                .len(),
2051            1
2052        );
2053    }
2054
2055    /// **The `!this.query` guard.** Without it a server stamps the default on every write and
2056    /// passes everything above while silently reverting a permission change a client made on
2057    /// purpose. Nothing in the response shows it: the update succeeds either way.
2058    #[tokio::test]
2059    async fn the_default_applies_on_create_and_never_on_update() {
2060        let storage = FakeStorage::new().with_schema(
2061            default_schema("Post")
2062                .with_clp(clp(r#"{"ACL":{"currentUser":{"read":true,"write":true}}}"#)),
2063        );
2064        let snap = snapshot(&storage).await;
2065        let options = opts();
2066
2067        // A supplies its own ACL, which suppresses the default: B may read, A may write.
2068        let a = AclScope::user("userA", vec![]).expect("scope");
2069        let ctx = Ctx::new(&storage, &snap, &a, &options);
2070        let created = create(
2071            &ctx,
2072            "Post",
2073            body(
2074                r#"{"title":"x","ACL":{"userA":{"read":true,"write":true},"userB":{"read":true}}}"#,
2075                OpPath::Create,
2076            ),
2077        )
2078        .await
2079        .expect("create");
2080
2081        let b = AclScope::user("userB", vec![]).expect("scope");
2082        let b_ctx = Ctx::new(&storage, &snap, &b, &options);
2083        assert_eq!(
2084            find(
2085                &b_ctx,
2086                "Post",
2087                ParsedWhere::default(),
2088                FindOptions::default()
2089            )
2090            .await
2091            .expect("find")
2092            .len(),
2093            1,
2094            "the client's own ACL must win over the class default on create"
2095        );
2096
2097        // An update to an unrelated field must not restamp the class default over it.
2098        update(
2099            &ctx,
2100            "Post",
2101            &created.object_id,
2102            body(r#"{"title":"y"}"#, OpPath::Update),
2103        )
2104        .await
2105        .expect("update");
2106        assert_eq!(
2107            find(
2108                &b_ctx,
2109                "Post",
2110                ParsedWhere::default(),
2111                FindOptions::default()
2112            )
2113            .await
2114            .expect("find")
2115            .len(),
2116            1,
2117            "the explicitly set ACL must survive an unrelated update"
2118        );
2119    }
2120
2121    /// A falsy `ACL` on the body does not suppress the default, because upstream's test is
2122    /// `!this.data.ACL` rather than a presence check. `{"ACL": null}` from a client therefore
2123    /// lands on the class default rather than on a public row.
2124    #[tokio::test]
2125    async fn a_falsy_acl_on_the_body_does_not_suppress_the_default() {
2126        let storage = FakeStorage::new().with_schema(
2127            default_schema("Post").with_clp(clp(r#"{"ACL":{"currentUser":{"read":true}}}"#)),
2128        );
2129        let snap = snapshot(&storage).await;
2130        let options = opts();
2131
2132        let a = AclScope::user("userA", vec![]).expect("scope");
2133        let ctx = Ctx::new(&storage, &snap, &a, &options);
2134        create(
2135            &ctx,
2136            "Post",
2137            body(r#"{"title":"x","ACL":null}"#, OpPath::Create),
2138        )
2139        .await
2140        .expect("create");
2141
2142        let b = AclScope::user("userB", vec![]).expect("scope");
2143        let ctx = Ctx::new(&storage, &snap, &b, &options);
2144        assert!(
2145            find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
2146                .await
2147                .expect("find")
2148                .is_empty()
2149        );
2150    }
2151
2152    /// **An `ACL` carrying an operation must not vanish.** `flatten_for_create` removes a `Delete`
2153    /// op from the body, so the key disappeared before `lower_acl` ran and the row was written
2154    /// with no permission columns, which is public. Upstream keeps the op object and writes two
2155    /// empty arrays, which is master-only. Measured at the pin on an ordinary class: an anonymous
2156    /// read of the created object answered 200 here and 404 there.
2157    ///
2158    /// Asserted on the stored columns rather than through a read, because "public" and
2159    /// "master-only" are the presence and the emptiness of the same two columns, and the
2160    /// distinction is exactly what a read cannot show for a master caller.
2161    #[tokio::test]
2162    async fn an_acl_operation_on_create_writes_empty_columns_rather_than_none() {
2163        for literal in [
2164            r#"{"title":"x","ACL":{"__op":"Delete"}}"#,
2165            r#"{"title":"x","ACL":{"__op":"Increment","amount":1}}"#,
2166        ] {
2167            let storage = FakeStorage::new().with_schema(default_schema("Post"));
2168            let snap = snapshot(&storage).await;
2169            let options = opts();
2170            let master = AclScope::Unrestricted;
2171            let ctx = Ctx::new(&storage, &snap, &master, &options);
2172
2173            create(&ctx, "Post", body(literal, OpPath::Create))
2174                .await
2175                .expect("create");
2176
2177            let rows = storage.rows("Post");
2178            assert_eq!(rows.len(), 1, "{literal}");
2179            for column in ["_rperm", "_wperm"] {
2180                assert!(
2181                    matches!(rows[0].get(column), Some(ParseValue::Array(a)) if a.is_empty()),
2182                    "{literal} must write an empty {column}, got {:?}",
2183                    rows[0].get(column)
2184                );
2185            }
2186        }
2187    }
2188
2189    /// The control for the test above, and the reason it cannot simply assert "columns exist": an
2190    /// ordinary create with no `ACL` writes **no** columns, which is what makes a row public.
2191    #[tokio::test]
2192    async fn a_create_with_no_acl_still_writes_no_columns() {
2193        let storage = FakeStorage::new().with_schema(default_schema("Post"));
2194        let snap = snapshot(&storage).await;
2195        let options = opts();
2196        let master = AclScope::Unrestricted;
2197        let ctx = Ctx::new(&storage, &snap, &master, &options);
2198
2199        create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
2200            .await
2201            .expect("create");
2202
2203        let rows = storage.rows("Post");
2204        assert!(rows[0].get("_rperm").is_none());
2205        assert!(rows[0].get("_wperm").is_none());
2206    }
2207
2208    /// `_Role`'s ACL is a required column, and the class default does not satisfy it: upstream
2209    /// runs `validateRequiredColumns` inside `validateSchema`, which precedes
2210    /// `setRequiredFieldsIfNeeded`. Asserting it here pins the ordering, which is otherwise
2211    /// invisible.
2212    #[tokio::test]
2213    async fn a_declared_default_does_not_satisfy_roles_required_acl() {
2214        let storage = FakeStorage::new().with_schema(
2215            default_schema("_Role").with_clp(clp(r#"{"ACL":{"currentUser":{"read":true}}}"#)),
2216        );
2217        let snap = snapshot(&storage).await;
2218        let options = opts();
2219        let master = AclScope::Unrestricted;
2220        let ctx = Ctx::new(&storage, &snap, &master, &options);
2221
2222        let e = create(&ctx, "_Role", body(r#"{"name":"Admins"}"#, OpPath::Create))
2223            .await
2224            .unwrap_err();
2225        assert_eq!(e.code, ErrorCode::IncorrectType);
2226        assert_eq!(e.message, "ACL is required.");
2227        assert!(storage.rows("_Role").is_empty());
2228    }
2229
2230    // -----------------------------------------------------------------------------------------
2231    // Atomic operations and schema reservation
2232    // -----------------------------------------------------------------------------------------
2233
2234    /// The 0.1.0 gap: the op decoder existed and the write path never called it.
2235    #[tokio::test]
2236    async fn operations_reach_storage_as_operations() {
2237        let storage = FakeStorage::new();
2238        let snap = snapshot(&storage).await;
2239        let options = opts();
2240        let master = AclScope::Unrestricted;
2241        let ctx = Ctx::new(&storage, &snap, &master, &options);
2242
2243        let created = create(
2244            &ctx,
2245            "Post",
2246            body(
2247                r#"{"views":{"__op":"Increment","amount":2},"tags":{"__op":"Add","objects":["a"]}}"#,
2248                OpPath::Create,
2249            ),
2250        )
2251        .await
2252        .expect("create");
2253
2254        let stored = storage.rows("Post");
2255        assert!(
2256            matches!(stored[0].get("views"), Some(ParseValue::Number(n)) if *n == 2.0),
2257            "an Increment must be flattened to a number, not stored as an op envelope"
2258        );
2259        assert!(matches!(stored[0].get("tags"), Some(ParseValue::Array(a)) if a.len() == 1));
2260        assert!(matches!(created.echoed.get("views"), Some(ParseValue::Number(n)) if *n == 2.0));
2261
2262        let snap = snapshot(&storage).await;
2263        let ctx = Ctx::new(&storage, &snap, &master, &options);
2264        let updated = update(
2265            &ctx,
2266            "Post",
2267            &created.object_id,
2268            body(
2269                r#"{"views":{"__op":"Increment","amount":3},"title":"plain"}"#,
2270                OpPath::Update,
2271            ),
2272        )
2273        .await
2274        .expect("update");
2275        assert!(
2276            matches!(updated.echoed.get("views"), Some(ParseValue::Number(n)) if *n == 5.0),
2277            "the response carries the post-update value"
2278        );
2279        assert!(
2280            updated.echoed.get("title").is_none(),
2281            "a plain set is not echoed"
2282        );
2283        assert!(
2284            matches!(storage.rows("Post")[0].get("views"), Some(ParseValue::Number(n)) if *n == 5.0)
2285        );
2286    }
2287
2288    #[tokio::test]
2289    async fn a_delete_op_unsets_the_field_and_echoes_nothing() {
2290        let storage = FakeStorage::new()
2291            .with_schema(default_schema("Post").with_field("title", FieldType::String))
2292            .with_row(
2293                "Post",
2294                row(vec![
2295                    ("objectId", ParseValue::String("p1".into())),
2296                    ("title", ParseValue::String("t".into())),
2297                ]),
2298            );
2299        let snap = snapshot(&storage).await;
2300        let options = opts();
2301        let master = AclScope::Unrestricted;
2302        let ctx = Ctx::new(&storage, &snap, &master, &options);
2303
2304        let response = update(
2305            &ctx,
2306            "Post",
2307            "p1",
2308            body(r#"{"title":{"__op":"Delete"}}"#, OpPath::Update),
2309        )
2310        .await
2311        .expect("update");
2312        assert!(response.echoed.is_empty());
2313        assert!(storage.rows("Post")[0].get("title").is_none());
2314    }
2315
2316    /// The field type is reserved atomically, before the row is written, so the loser of a race
2317    /// fails rather than overwriting the winner's type.
2318    #[tokio::test]
2319    async fn a_type_conflict_fails_the_write_before_the_row_is_inserted() {
2320        let storage = FakeStorage::new()
2321            .with_schema(default_schema("Post").with_field("views", FieldType::Number));
2322        let snap = SchemaSnapshot::from_classes(vec![default_schema("Post")]);
2323        let options = opts();
2324        let master = AclScope::Unrestricted;
2325        let ctx = Ctx::new(&storage, &snap, &master, &options);
2326
2327        // The snapshot does not know about `views`, so validation passes and the reservation is
2328        // what catches the conflict. That is the race, reproduced deterministically.
2329        let e = create(&ctx, "Post", body(r#"{"views":"text"}"#, OpPath::Create))
2330            .await
2331            .unwrap_err();
2332        assert_eq!(e.code, ErrorCode::IncorrectType);
2333        assert_eq!(
2334            e.message,
2335            "schema mismatch for Post.views; expected Number but got String"
2336        );
2337        assert!(storage.rows("Post").is_empty(), "no row was inserted");
2338    }
2339
2340    #[tokio::test]
2341    async fn a_write_of_only_nulls_still_creates_the_class() {
2342        let storage = FakeStorage::new();
2343        let snap = snapshot(&storage).await;
2344        let options = opts();
2345        let master = AclScope::Unrestricted;
2346        let ctx = Ctx::new(&storage, &snap, &master, &options);
2347
2348        create(&ctx, "Post", body(r#"{"nothing":null}"#, OpPath::Create))
2349            .await
2350            .expect("create");
2351        let schema = storage
2352            .schema("Post")
2353            .expect("a null-only write must still leave a schema row behind");
2354        assert!(schema.field("objectId").is_some());
2355        assert!(schema.field("nothing").is_none(), "null infers no type");
2356    }
2357
2358    // -----------------------------------------------------------------------------------------
2359    // Relations
2360    // -----------------------------------------------------------------------------------------
2361
2362    #[tokio::test]
2363    async fn a_relation_write_lands_in_the_join_table_and_reads_back_through_related_to() {
2364        let storage = FakeStorage::new()
2365            .with_schema(default_schema("_Role"))
2366            .with_schema(default_schema("_User"))
2367            .with_row(
2368                "_User",
2369                row(vec![("objectId", ParseValue::String("u1".into()))]),
2370            );
2371        let snap = snapshot(&storage).await;
2372        let options = opts();
2373        let master = AclScope::Unrestricted;
2374        let ctx = Ctx::new(&storage, &snap, &master, &options);
2375
2376        let created = create(
2377            &ctx,
2378            "_Role",
2379            body(
2380                r#"{"name":"admins","ACL":{"*":{"read":true}},
2381                    "users":{"__op":"AddRelation","objects":[
2382                        {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2383                OpPath::Create,
2384            ),
2385        )
2386        .await
2387        .expect("create");
2388
2389        let stored = storage.rows("_Role");
2390        assert!(
2391            stored[0].get("users").is_none(),
2392            "a Relation field has no column"
2393        );
2394        let joins = storage.rows("_Join:users:_Role");
2395        assert_eq!(joins.len(), 1);
2396        assert!(matches!(joins[0].get("relatedId"), Some(ParseValue::String(id)) if id == "u1"));
2397        assert!(
2398            matches!(joins[0].get("owningId"), Some(ParseValue::String(id)) if *id == created.object_id)
2399        );
2400        assert!(
2401            storage.schema("_Join:users:_Role").is_none(),
2402            "a join collection has no _SCHEMA row"
2403        );
2404
2405        // The same membership added twice is one row.
2406        let snap = snapshot(&storage).await;
2407        let ctx = Ctx::new(&storage, &snap, &master, &options);
2408        update(
2409            &ctx,
2410            "_Role",
2411            &created.object_id,
2412            body(
2413                r#"{"users":{"__op":"AddRelation","objects":[
2414                    {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2415                OpPath::Update,
2416            ),
2417        )
2418        .await
2419        .expect("update");
2420        assert_eq!(storage.rows("_Join:users:_Role").len(), 1);
2421
2422        // And it reads back.
2423        let query = format!(
2424            r#"{{"$relatedTo":{{"object":{{"__type":"Pointer","className":"_Role","objectId":"{}"}},"key":"users"}}}}"#,
2425            created.object_id
2426        );
2427        let members = find(&ctx, "_User", where_(&query), FindOptions::default())
2428            .await
2429            .expect("find");
2430        assert_eq!(members.len(), 1);
2431
2432        // Removing the membership empties it.
2433        update(
2434            &ctx,
2435            "_Role",
2436            &created.object_id,
2437            body(
2438                r#"{"users":{"__op":"RemoveRelation","objects":[
2439                    {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2440                OpPath::Update,
2441            ),
2442        )
2443        .await
2444        .expect("update");
2445        assert!(storage.rows("_Join:users:_Role").is_empty());
2446    }
2447
2448    /// A caller who cannot read the owning object gets an empty result, not an error, so the
2449    /// relation is not a membership oracle.
2450    #[tokio::test]
2451    async fn a_related_to_the_caller_cannot_read_yields_empty_rather_than_an_error() {
2452        let storage = FakeStorage::new()
2453            .with_schema(default_schema("_Role"))
2454            .with_schema(default_schema("_User"))
2455            .with_row(
2456                "_Role",
2457                row(vec![
2458                    ("objectId", ParseValue::String("r1".into())),
2459                    ("_rperm", strings(&["u2"])),
2460                ]),
2461            )
2462            .with_row(
2463                "_User",
2464                row(vec![("objectId", ParseValue::String("u1".into()))]),
2465            )
2466            .with_row(
2467                "_Join:users:_Role",
2468                row(vec![
2469                    ("relatedId", ParseValue::String("u1".into())),
2470                    ("owningId", ParseValue::String("r1".into())),
2471                ]),
2472            );
2473        let snap = snapshot(&storage).await;
2474        let options = opts();
2475        let query = r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#;
2476
2477        let outsider = AclScope::user("u3", vec![]).expect("scope");
2478        let ctx = Ctx::new(&storage, &snap, &outsider, &options);
2479        let results = find(&ctx, "_User", where_(query), FindOptions::default())
2480            .await
2481            .expect("a denied relation is empty, not an error");
2482        assert!(results.is_empty());
2483
2484        // The caller who can read the role sees the membership, which is what proves the empty
2485        // result above was the authorization and not a broken join read.
2486        let insider = AclScope::user("u2", vec![]).expect("scope");
2487        let ctx = Ctx::new(&storage, &snap, &insider, &options);
2488        assert_eq!(
2489            find(&ctx, "_User", where_(query), FindOptions::default())
2490                .await
2491                .expect("find")
2492                .len(),
2493            1
2494        );
2495    }
2496
2497    /// A protected relation key on the owning class is a refusal rather than an empty result,
2498    /// because the key itself is the disclosure.
2499    #[tokio::test]
2500    async fn a_related_to_on_a_protected_key_is_forbidden() {
2501        let storage = FakeStorage::new()
2502            .with_schema(default_schema("_User"))
2503            .with_schema(
2504                default_schema("_Role")
2505                    .with_field(
2506                        "users",
2507                        FieldType::Relation {
2508                            target_class: "_User".into(),
2509                        },
2510                    )
2511                    .with_clp(clp(r#"{"protectedFields":{"*":["users"]}}"#)),
2512            )
2513            .with_row(
2514                "_Role",
2515                row(vec![("objectId", ParseValue::String("r1".into()))]),
2516            );
2517        let snap = snapshot(&storage).await;
2518        let options = opts();
2519        let user = AclScope::user("u1", vec![]).expect("scope");
2520        let ctx = Ctx::new(&storage, &snap, &user, &options);
2521        let query = r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#;
2522        let e = find(&ctx, "_User", where_(query), FindOptions::default())
2523            .await
2524            .unwrap_err();
2525        assert_eq!(e.code, ErrorCode::OperationForbidden);
2526        assert_eq!(e.message, "Permission denied");
2527
2528        let disclosing = disclosing_opts();
2529        let disclosed = Ctx::new(&storage, &snap, &user, &disclosing);
2530        assert_eq!(
2531            find(&disclosed, "_User", where_(query), FindOptions::default())
2532                .await
2533                .unwrap_err()
2534                .message,
2535            "This user is not allowed to query users on class _Role"
2536        );
2537    }
2538
2539    #[tokio::test]
2540    async fn a_constraint_on_a_relation_field_is_the_reverse_join() {
2541        let storage = FakeStorage::new()
2542            .with_schema(default_schema("_User"))
2543            .with_schema(default_schema("_Role").with_field(
2544                "users",
2545                FieldType::Relation {
2546                    target_class: "_User".into(),
2547                },
2548            ))
2549            .with_row(
2550                "_Role",
2551                row(vec![("objectId", ParseValue::String("r1".into()))]),
2552            )
2553            .with_row(
2554                "_Role",
2555                row(vec![("objectId", ParseValue::String("r2".into()))]),
2556            )
2557            .with_row(
2558                "_Join:users:_Role",
2559                row(vec![
2560                    ("relatedId", ParseValue::String("u1".into())),
2561                    ("owningId", ParseValue::String("r1".into())),
2562                ]),
2563            );
2564        let snap = snapshot(&storage).await;
2565        let options = opts();
2566        let master = AclScope::Unrestricted;
2567        let ctx = Ctx::new(&storage, &snap, &master, &options);
2568
2569        let results = find(
2570            &ctx,
2571            "_Role",
2572            where_(r#"{"users":{"__type":"Pointer","className":"_User","objectId":"u1"}}"#),
2573            FindOptions::default(),
2574        )
2575        .await
2576        .expect("find");
2577        assert_eq!(results.len(), 1);
2578        assert!(matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "r1"));
2579    }
2580
2581    // -----------------------------------------------------------------------------------------
2582    // include
2583    // -----------------------------------------------------------------------------------------
2584
2585    /// Ranked hazard 6: an included pointer is a full query against the target class with the
2586    /// caller's own auth. Grafting the row in without that is the classic Parse data leak.
2587    #[tokio::test]
2588    async fn include_applies_the_target_class_acl() {
2589        let storage = FakeStorage::new()
2590            .with_schema(default_schema("_User").with_field("nickname", FieldType::String))
2591            .with_schema(default_schema("Post").with_field(
2592                "author",
2593                FieldType::Pointer {
2594                    target_class: "_User".into(),
2595                },
2596            ))
2597            .with_row(
2598                "Post",
2599                row(vec![
2600                    ("objectId", ParseValue::String("p1".into())),
2601                    ("author", pointer("_User", "u1")),
2602                ]),
2603            )
2604            .with_row(
2605                "_User",
2606                row(vec![
2607                    ("objectId", ParseValue::String("u1".into())),
2608                    ("nickname", ParseValue::String("nick".into())),
2609                    ("_hashed_password", ParseValue::String("hash".into())),
2610                    ("sessionToken", ParseValue::String("r:t".into())),
2611                    ("_rperm", strings(&["u1"])),
2612                ]),
2613            );
2614        let snap = snapshot(&storage).await;
2615        let options = opts();
2616        let include = FindOptions {
2617            include: vec![vec!["author".to_string()]],
2618            ..Default::default()
2619        };
2620
2621        let anon = AclScope::Anonymous;
2622        let ctx = Ctx::new(&storage, &snap, &anon, &options);
2623        let results = find(&ctx, "Post", ParsedWhere::default(), include.clone())
2624            .await
2625            .expect("find");
2626        assert_eq!(results.len(), 1);
2627        assert!(
2628            results[0].get("author").is_none(),
2629            "an unreadable pointer is dropped rather than expanded or left as a pointer"
2630        );
2631
2632        let owner = AclScope::user("u1", vec![]).expect("scope");
2633        let ctx = Ctx::new(&storage, &snap, &owner, &options);
2634        let results = find(&ctx, "Post", ParsedWhere::default(), include)
2635            .await
2636            .expect("find");
2637        match results[0].get("author") {
2638            Some(ParseValue::Object(author)) => {
2639                assert!(
2640                    matches!(author.get("nickname"), Some(ParseValue::String(n)) if n == "nick")
2641                );
2642                assert!(author.get("_hashed_password").is_none());
2643                assert!(author.get("sessionToken").is_none());
2644                assert!(
2645                    matches!(author.get("__type"), Some(ParseValue::String(t)) if t == "Object")
2646                );
2647            }
2648            other => panic!("expected an expanded author, got {other:?}"),
2649        }
2650    }
2651
2652    #[tokio::test]
2653    async fn a_dotted_include_resolves_parents_before_children() {
2654        let storage = FakeStorage::new()
2655            .with_schema(default_schema("Company").with_field("name", FieldType::String))
2656            .with_schema(default_schema("_User").with_field(
2657                "company",
2658                FieldType::Pointer {
2659                    target_class: "Company".into(),
2660                },
2661            ))
2662            .with_schema(default_schema("Post").with_field(
2663                "author",
2664                FieldType::Pointer {
2665                    target_class: "_User".into(),
2666                },
2667            ))
2668            .with_row(
2669                "Post",
2670                row(vec![
2671                    ("objectId", ParseValue::String("p1".into())),
2672                    ("author", pointer("_User", "u1")),
2673                ]),
2674            )
2675            .with_row(
2676                "_User",
2677                row(vec![
2678                    ("objectId", ParseValue::String("u1".into())),
2679                    ("company", pointer("Company", "c1")),
2680                ]),
2681            )
2682            .with_row(
2683                "Company",
2684                row(vec![
2685                    ("objectId", ParseValue::String("c1".into())),
2686                    ("name", ParseValue::String("Acme".into())),
2687                ]),
2688            );
2689        let snap = snapshot(&storage).await;
2690        let options = opts();
2691        let anon = AclScope::Anonymous;
2692        let ctx = Ctx::new(&storage, &snap, &anon, &options);
2693
2694        let results = find(
2695            &ctx,
2696            "Post",
2697            ParsedWhere::default(),
2698            FindOptions {
2699                include: crate::query_parse::parse_include("author.company").expect("include"),
2700                ..Default::default()
2701            },
2702        )
2703        .await
2704        .expect("find");
2705
2706        let Some(ParseValue::Object(author)) = results[0].get("author") else {
2707            panic!("author should be expanded");
2708        };
2709        let Some(ParseValue::Object(company)) = author.get("company") else {
2710            panic!("company should be expanded");
2711        };
2712        assert!(matches!(company.get("name"), Some(ParseValue::String(n)) if n == "Acme"));
2713    }
2714
2715    /// `allowClientClassCreation`, whose default is `false`. A server missing this check is more
2716    /// permissive than a stock parse-server, so the assertion that matters is that the *default*
2717    /// options refuse, not that the option works when set.
2718    #[tokio::test]
2719    async fn a_client_cannot_bring_a_class_into_existence_at_the_default() {
2720        let storage = FakeStorage::new();
2721        let snap = snapshot(&storage).await;
2722        let options = opts();
2723        let anon = AclScope::Anonymous;
2724        let ctx = Ctx::new(&storage, &snap, &anon, &options);
2725
2726        let err = create(&ctx, "BrandNew", body(r#"{"x":1}"#, OpPath::Create))
2727            .await
2728            .expect_err("a client must not create a class at the default");
2729        assert_eq!(err.code, ErrorCode::OperationForbidden);
2730        assert!(
2731            storage.schema("BrandNew").is_none(),
2732            "the refusal must leave no _SCHEMA row behind"
2733        );
2734    }
2735
2736    /// The three exemptions, each for a different reason: the option, the master key, and the
2737    /// classes Parse defines itself. The last one is what keeps signup working with the option off.
2738    #[tokio::test]
2739    async fn master_the_option_and_the_system_classes_are_all_exempt() {
2740        for (label, scope, allow, class) in [
2741            ("option on", AclScope::Anonymous, true, "BrandNew"),
2742            ("master", AclScope::Unrestricted, false, "BrandNew"),
2743            ("system class", AclScope::Anonymous, false, "_User"),
2744        ] {
2745            let storage = FakeStorage::new();
2746            let snap = snapshot(&storage).await;
2747            let options = PermissionOptions {
2748                allow_client_class_creation: allow,
2749                ..PermissionOptions::default()
2750            };
2751            let ctx = Ctx::new(&storage, &snap, &scope, &options);
2752            create(&ctx, class, body(r#"{"x":1}"#, OpPath::Create))
2753                .await
2754                .unwrap_or_else(|e| panic!("{label} should be allowed to create {class}: {e:?}"));
2755        }
2756    }
2757}
2758
2759#[cfg(test)]
2760mod relation_schema_tests {
2761    use super::tests_support::*;
2762    use super::*;
2763    use crate::testing::FakeStorage;
2764    use parse_rust_core::op::OpPath;
2765
2766    /// An `AddRelation` reserves `Relation<Target>` for the field, which is what makes a later
2767    /// `$relatedTo` against a user-defined class resolve at all. The op is stripped from the row
2768    /// write, but only after the schema has been reserved from it.
2769    #[tokio::test]
2770    async fn a_relation_op_reserves_the_field_type_before_it_is_stripped() {
2771        let storage = FakeStorage::new()
2772            .with_schema(default_schema("_User"))
2773            .with_row("_User", single("objectId", ParseValue::String("u1".into())));
2774        let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2775        let options = PermissionOptions::default();
2776        let master = AclScope::Unrestricted;
2777        let ctx = Ctx::new(&storage, &snap, &master, &options);
2778
2779        let created = create(
2780            &ctx,
2781            "Team",
2782            decode(
2783                r#"{"name":"core","members":{"__op":"AddRelation","objects":[
2784                    {"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
2785                OpPath::Create,
2786            ),
2787        )
2788        .await
2789        .expect("create");
2790
2791        let schema = storage.schema("Team").expect("class reserved");
2792        assert_eq!(
2793            schema.field("members"),
2794            Some(&FieldType::Relation {
2795                target_class: "_User".into()
2796            }),
2797            "without this a $relatedTo against Team.members resolves to nothing forever"
2798        );
2799        assert!(storage.rows("Team")[0].get("members").is_none());
2800        assert_eq!(storage.rows("_Join:members:Team").len(), 1);
2801
2802        // And the reverse read works, which is the observable consequence.
2803        let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2804        let ctx = Ctx::new(&storage, &snap, &master, &options);
2805        let query = format!(
2806            r#"{{"$relatedTo":{{"object":{{"__type":"Pointer","className":"Team","objectId":"{}"}},"key":"members"}}}}"#,
2807            created.object_id
2808        );
2809        assert_eq!(
2810            find(
2811                &ctx,
2812                "_User",
2813                parse_json_where(&query),
2814                FindOptions::default()
2815            )
2816            .await
2817            .expect("find")
2818            .len(),
2819            1
2820        );
2821    }
2822}
2823
2824#[cfg(test)]
2825mod tests_support {
2826    use super::*;
2827    use crate::query_parse::parse_where;
2828    use crate::write::decode_write_body;
2829    use parse_rust_core::op::OpPath;
2830
2831    pub fn single(key: &str, value: ParseValue) -> ParseMap {
2832        let mut m = ParseMap::new();
2833        m.insert(key.to_string(), value);
2834        m
2835    }
2836
2837    pub fn decode(json: &str, path: OpPath) -> WriteBody {
2838        decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
2839    }
2840
2841    pub fn parse_json_where(json: &str) -> ParsedWhere {
2842        parse_where(&serde_json::from_str(json).expect("test literal")).expect("parse")
2843    }
2844}
2845
2846#[cfg(test)]
2847mod write_edge_tests {
2848    use super::tests_support::*;
2849    use super::*;
2850    use crate::testing::FakeStorage;
2851    use parse_rust_core::op::OpPath;
2852
2853    /// A **falsy** `ACL` leaves the stored permissions alone rather than clearing them. Clearing
2854    /// them would lock every principal out of a row they still own, and on `_User` it disables the
2855    /// account outright: an empty ACL reads as "disabled" and refuses every later login.
2856    ///
2857    /// **The loop is the test.** This asserted `null` alone until a review, and the other three
2858    /// falsy values fell through to an unconditional write that set both columns to `[]`.
2859    /// Upstream's test is `if (!ACL)`, so all four behave the same there. Checking one value is
2860    /// exactly what let the other three through.
2861    #[tokio::test]
2862    async fn a_falsy_acl_on_an_update_does_not_clear_the_permissions() {
2863        for body in [
2864            r#"{"ACL":null,"title":"t"}"#,
2865            r#"{"ACL":false,"title":"t"}"#,
2866            r#"{"ACL":0,"title":"t"}"#,
2867            r#"{"ACL":"","title":"t"}"#,
2868        ] {
2869            assert_falsy_acl_preserves_permissions(body).await;
2870        }
2871    }
2872
2873    async fn assert_falsy_acl_preserves_permissions(body: &str) {
2874        let mut existing = single("objectId", ParseValue::String("p1".into()));
2875        existing.insert(
2876            "_rperm".to_string(),
2877            ParseValue::Array(vec![ParseValue::String("u1".into())]),
2878        );
2879        existing.insert(
2880            "_wperm".to_string(),
2881            ParseValue::Array(vec![ParseValue::String("u1".into())]),
2882        );
2883        let storage = FakeStorage::new()
2884            .with_schema(default_schema("Post").with_field("title", FieldType::String))
2885            .with_row("Post", existing);
2886        let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2887        let options = PermissionOptions::default();
2888        let master = AclScope::Unrestricted;
2889        let ctx = Ctx::new(&storage, &snap, &master, &options);
2890
2891        update(&ctx, "Post", "p1", decode(body, OpPath::Update))
2892            .await
2893            .expect("update");
2894
2895        let stored = &storage.rows("Post")[0];
2896        assert!(
2897            matches!(stored.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 1),
2898            "the existing permissions must survive a falsy ACL: {body}"
2899        );
2900    }
2901
2902    /// An update to a class nobody has written yet answers `OBJECT_NOT_FOUND` **and leaves the
2903    /// class behind**, whatever the body contains.
2904    ///
2905    /// This test asserted the opposite until a review caught it, and the assertion was wrong in a
2906    /// way that hid a second problem: the outcome was body-dependent. An empty update really did
2907    /// leave nothing, while an update naming a new field created the class as a side effect of
2908    /// reserving the field. Upstream has one answer for both, because `enforceClassExists` runs
2909    /// from `validateSchema` before any field is looked at (`SchemaController.js:1288`,
2910    /// `RestWrite.js:127-128`).
2911    #[tokio::test]
2912    async fn an_update_to_a_missing_class_creates_the_class_and_then_finds_nothing() {
2913        // Including a body that is **rejected**, which is the case the first version of this fix
2914        // still got wrong: the class creation sat behind `validate_write_fields`, so a bad field
2915        // name skipped it. Upstream's `enforceClassExists` runs before any field is inspected, so
2916        // all three of these leave the class behind and only the error differs.
2917        for body in [r#"{}"#, r#"{"title":"a"}"#, r#"{"bad-key":1}"#] {
2918            let storage = FakeStorage::new();
2919            let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2920            let options = PermissionOptions::default();
2921            let master = AclScope::Unrestricted;
2922            let ctx = Ctx::new(&storage, &snap, &master, &options);
2923
2924            let e = update(&ctx, "Ghost", "p1", decode(body, OpPath::Update))
2925                .await
2926                .unwrap_err();
2927            let expected = if body.contains("bad-key") {
2928                ErrorCode::InvalidKeyName
2929            } else {
2930                ErrorCode::ObjectNotFound
2931            };
2932            assert_eq!(e.code, expected, "for body {body}");
2933            assert!(
2934                storage.schema("Ghost").is_some(),
2935                "the schema row survives the failed update, for body {body}"
2936            );
2937        }
2938    }
2939
2940    /// An ACL written on a create round-trips through the two storage columns.
2941    #[tokio::test]
2942    async fn an_acl_on_an_update_replaces_both_columns() {
2943        let storage = FakeStorage::new()
2944            .with_schema(default_schema("Post"))
2945            .with_row("Post", single("objectId", ParseValue::String("p1".into())));
2946        let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
2947        let options = PermissionOptions::default();
2948        let master = AclScope::Unrestricted;
2949        let ctx = Ctx::new(&storage, &snap, &master, &options);
2950
2951        update(
2952            &ctx,
2953            "Post",
2954            "p1",
2955            decode(
2956                r#"{"ACL":{"u1":{"read":true,"write":true},"*":{"read":true}}}"#,
2957                OpPath::Update,
2958            ),
2959        )
2960        .await
2961        .expect("update");
2962
2963        let stored = &storage.rows("Post")[0];
2964        assert!(stored.get("ACL").is_none(), "ACL is not a stored column");
2965        assert!(matches!(stored.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 2));
2966        assert!(matches!(stored.get("_wperm"), Some(ParseValue::Array(a)) if a.len() == 1));
2967    }
2968}