Skip to main content

parse_rust_rest/
relations.rs

1//! Relations: the join tables, and the query constructs that read them.
2//!
3//! **A `Relation` field has no column.** It is skipped on write, stored as `relation<Target>` in
4//! `_SCHEMA`, and synthesized from the schema on read. Membership lives in
5//! `_Join:<key>:<className>` as documents of exactly `{relatedId, owningId}`
6//! (`DatabaseController.js:319-321`, `:418-420`, `:794-830`), and **those collections have no
7//! `_SCHEMA` row at all**. `parse_rust_storage::join_schema` builds the shape in memory for that
8//! reason: writing a schema row for a join table would add a class every parse-server node
9//! reading the same database would then see.
10//!
11//! **The writes here are not atomic with the parent write.** The row write, the join upsert and
12//! the schema reservation are three operations, and without transactions any of them can fail
13//! independently, leaving a membership recorded against a row that was never written or a row
14//! written without its membership. Upstream has the same exposure on a non-replica-set
15//! deployment. It is stated here rather than left to be discovered.
16
17use parse_rust_core::{js_number, ErrorCode, ErrorDetail, ParseError, ParseMap, ParseValue};
18use parse_rust_storage::{
19    join_schema, Clause, Comparison, Constraint, Query, QueryOptions, StorageAdapter,
20};
21
22/// One pending membership change, stripped out of a write body.
23#[derive(Debug, Clone)]
24pub struct RelationUpdate {
25    pub key: String,
26    pub kind: RelationOpKind,
27    /// The objectIds of the related objects, in the order the client sent them.
28    pub related_ids: Vec<String>,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum RelationOpKind {
33    Add,
34    Remove,
35}
36
37/// What a `$relatedTo` resolved to.
38///
39/// Not `Vec<String>` with an empty vector standing in for "denied", because the two have to be
40/// distinguishable at the call site even though they narrow the query identically. A caller who
41/// cannot read the owning object gets an empty `objectId $in` rather than an error
42/// (`DatabaseController.js:1209-1213`), so the relation cannot be used as a membership oracle.
43#[derive(Debug, Clone)]
44pub enum RelatedToOutcome {
45    Ids(Vec<String>),
46    DeniedYieldEmpty,
47}
48
49impl RelatedToOutcome {
50    pub fn ids(&self) -> &[String] {
51        match self {
52            RelatedToOutcome::Ids(ids) => ids,
53            RelatedToOutcome::DeniedYieldEmpty => &[],
54        }
55    }
56}
57
58/// The find options a join-table read uses.
59///
60/// **Not `QueryOptions::default()`**, whose limit is 100. A role with more than a hundred members
61/// would silently lose the rest, and the loss would look like a permission problem. Upstream
62/// passes an empty options object, meaning unbounded (`DatabaseController.js:1030`).
63fn join_query_options(keys: &[&str]) -> QueryOptions {
64    QueryOptions {
65        limit: None,
66        skip: None,
67        order: Vec::new(),
68        keys: Some(keys.iter().map(|k| k.to_string()).collect()),
69        case_insensitive: false,
70    }
71}
72
73/// Strip `AddRelation` and `RemoveRelation` out of a write body, including inside a `Batch`.
74///
75/// `collectRelationUpdates` (`DatabaseController.js:732-765`). Note that a `Batch` containing a
76/// relation op removes the **whole key** from the write, so a batch mixing a relation op with
77/// anything else loses the rest. That is upstream's `deleteMe.push(key)` and it is reproduced.
78pub fn collect_relation_updates(body: &mut crate::WriteBody) -> Vec<RelationUpdate> {
79    use parse_rust_core::{FieldWrite, Op};
80
81    fn walk(key: &str, op: &Op, out: &mut Vec<RelationUpdate>) -> bool {
82        match op {
83            Op::AddRelation(objects) => {
84                out.push(RelationUpdate {
85                    key: key.to_string(),
86                    kind: RelationOpKind::Add,
87                    related_ids: related_object_ids(objects),
88                });
89                true
90            }
91            Op::RemoveRelation(objects) => {
92                out.push(RelationUpdate {
93                    key: key.to_string(),
94                    kind: RelationOpKind::Remove,
95                    related_ids: related_object_ids(objects),
96                });
97                true
98            }
99            Op::Batch(ops) => {
100                let mut any = false;
101                for inner in ops {
102                    any |= walk(key, inner, out);
103                }
104                any
105            }
106            _ => false,
107        }
108    }
109
110    let mut updates = Vec::new();
111    let mut remove: Vec<String> = Vec::new();
112    for (key, write) in body.iter() {
113        if let FieldWrite::Op(op) = write {
114            if walk(key, op, &mut updates) {
115                remove.push(key.clone());
116            }
117        }
118    }
119    for key in remove {
120        body.shift_remove(&key);
121    }
122    updates
123}
124
125/// The objectIds inside a relation op's `objects` array.
126///
127/// Upstream reads `object.objectId` without checking `__type`, so a bare `{objectId: "x"}` works
128/// the same as a Pointer. An element with no objectId is skipped rather than written as a join
129/// document with a null `relatedId`, which is what upstream's `undefined` would store.
130fn related_object_ids(objects: &[ParseValue]) -> Vec<String> {
131    objects
132        .iter()
133        .filter_map(|v| match v {
134            ParseValue::Pointer { object_id, .. } => Some(object_id.clone()),
135            ParseValue::Object(map) => match map.get("objectId") {
136                Some(ParseValue::String(id)) => Some(id.clone()),
137                _ => None,
138            },
139            _ => None,
140        })
141        .collect()
142}
143
144/// Apply membership changes, after the row write has succeeded.
145///
146/// `handleRelationUpdates` (`DatabaseController.js:769-830`). An add is an upsert, so adding a
147/// user to a role twice is one membership. A remove that matches nothing is not an error:
148/// upstream swallows `OBJECT_NOT_FOUND` (`:823-829`).
149pub async fn apply_relation_updates<S: StorageAdapter>(
150    storage: &S,
151    class_name: &str,
152    object_id: &str,
153    updates: &[RelationUpdate],
154) -> Result<(), ParseError> {
155    for update in updates {
156        let schema = join_schema(class_name, &update.key);
157        for related_id in &update.related_ids {
158            let mut doc = ParseMap::new();
159            doc.insert(
160                "relatedId".to_string(),
161                ParseValue::String(related_id.clone()),
162            );
163            doc.insert(
164                "owningId".to_string(),
165                ParseValue::String(object_id.to_string()),
166            );
167            let query = Query::from_constraints(vec![
168                Constraint::equal("relatedId", ParseValue::String(related_id.clone())),
169                Constraint::equal("owningId", ParseValue::String(object_id.to_string())),
170            ]);
171            match update.kind {
172                RelationOpKind::Add => storage.upsert_one(&schema, &query, &doc).await?,
173                RelationOpKind::Remove => match storage.delete(&schema, &query).await {
174                    Ok(_) => {}
175                    Err(e) if e.code == ErrorCode::ObjectNotFound => {}
176                    Err(e) => return Err(e),
177                },
178            }
179        }
180    }
181    Ok(())
182}
183
184/// The related objectIds of one owning object. `relatedIds` (`DatabaseController.js:1015-1032`).
185pub async fn related_ids<S: StorageAdapter>(
186    storage: &S,
187    owning_class: &str,
188    key: &str,
189    owning_id: &str,
190) -> Result<Vec<String>, ParseError> {
191    let schema = join_schema(owning_class, key);
192    let query = Query::from_constraints(vec![Constraint::equal(
193        "owningId",
194        ParseValue::String(owning_id.to_string()),
195    )]);
196    let rows = storage
197        .find(&schema, &query, &join_query_options(&["relatedId"]))
198        .await?;
199    Ok(string_column(rows, "relatedId"))
200}
201
202/// The owning objectIds that relate to any of these ids. `owningIds`
203/// (`DatabaseController.js:1036-1045`).
204pub async fn owning_ids<S: StorageAdapter>(
205    storage: &S,
206    owning_class: &str,
207    key: &str,
208    related_ids: &[String],
209) -> Result<Vec<String>, ParseError> {
210    let schema = join_schema(owning_class, key);
211    let query = Query::from_constraints(vec![Constraint::one_of(
212        "relatedId",
213        related_ids
214            .iter()
215            .map(|id| ParseValue::String(id.clone()))
216            .collect(),
217    )]);
218    let rows = storage
219        .find(&schema, &query, &join_query_options(&["owningId"]))
220        .await?;
221    Ok(string_column(rows, "owningId"))
222}
223
224fn string_column(rows: Vec<ParseMap>, key: &str) -> Vec<String> {
225    rows.into_iter()
226        .filter_map(|row| match row.get(key) {
227            Some(ParseValue::String(s)) => Some(s.clone()),
228            _ => None,
229        })
230        .collect()
231}
232
233/// `authorizeRelatedToQuery` (`DatabaseController.js:1260-1308`), run **before** the join table
234/// is read.
235///
236/// Two checks, and neither is redundant. The relation key must not be a protected field on the
237/// *owning* class, because the downstream protected-field filter only ever applies to the class
238/// being queried. And the caller must be able to read the owning object, because none of the
239/// owning class's CLP or ACL is otherwise consulted.
240///
241/// `can_read_owning` performs the second check as a full read with the caller's own auth. It is a
242/// parameter rather than a call into the pipeline so that the recursion stays at the one place
243/// that owns it.
244pub async fn authorize_related_to<F, Fut>(
245    owning_class: &str,
246    relation_key: &str,
247    owning_protected_fields: &[String],
248    detail: ErrorDetail,
249    can_read_owning: F,
250) -> Result<bool, ParseError>
251where
252    F: FnOnce() -> Fut,
253    Fut: std::future::Future<Output = Result<bool, ParseError>>,
254{
255    let root = relation_key.split('.').next().unwrap_or(relation_key);
256    if owning_protected_fields
257        .iter()
258        .any(|f| f == relation_key || f == root)
259    {
260        // `createSanitizedError` (`DatabaseController.js:1279-1283`).
261        return Err(ParseError::permission_denied(
262            ErrorCode::OperationForbidden,
263            format!("This user is not allowed to query {relation_key} on class {owning_class}"),
264            detail,
265        ));
266    }
267    can_read_owning().await
268}
269
270/// Which reverse-join read a constraint on a `Relation`-typed field asks for.
271///
272/// `reduceInRelation` (`DatabaseController.js:1050-1143`). Note the fourth case: **any other
273/// constraint on a relation field yields no results at all**, because upstream falls into its
274/// `else` branch with an empty related-id list. That is reproduced rather than turned into an
275/// error, since it narrows rather than broadens and a client can already observe it.
276#[derive(Debug, Clone)]
277pub enum RelationConstraint {
278    /// The owning objects related to any of these ids.
279    OwnersOf(Vec<String>),
280    /// The complement: `objectId $nin`.
281    NotOwnersOf(Vec<String>),
282}
283
284/// The `objectId` of an operand, **with no tag check**.
285///
286/// **Upstream reads the key off the raw REST JSON and never runs an atom transform here**
287/// (`DatabaseController.js:1092-1101`: `relatedIds = [query[key].objectId]`, and `r => r.objectId`
288/// across `$in` and `$nin`). `reduceInRelation` runs on the REST query, before anything reaches the
289/// Mongo lowering, so the value it sees is the object the client sent, and `r.objectId` asks
290/// nothing about `__type`.
291///
292/// Both spellings are read because both occur. A parsed operand is a raw `Object`, since query
293/// operands are no longer decoded before the schema is known; a constraint the CLP and ACL paths
294/// build in Rust carries a real `Pointer`.
295///
296/// **`null` is refused rather than skipped.** It is the one operand upstream cannot read
297/// `.objectId` from, because it is the one value JavaScript will not box, so it raises an uncaught
298/// `TypeError` and the request 500s (`DatabaseController.js:1096-1104`; reported upstream as
299/// parse-community/parse-server#10637). Every other unusable operand is harmless there: `7` and
300/// `{"foo":1}` both yield `undefined`, which contributes no id.
301///
302/// Skipping it silently is the one answer that must not be given. A dropped element of a `$nin`
303/// leaves an empty exclusion list, which excludes nobody, so `{"friends":{"$nin":[null]}}` returns
304/// every otherwise-readable row where upstream returns none at all. Refusing narrows instead, and
305/// says why.
306fn object_id_of(value: &ParseValue) -> Result<Option<String>, ParseError> {
307    match value {
308        ParseValue::Null => Err(ParseError::invalid_json(
309            "cannot use null in a constraint on a Relation field",
310        )),
311        ParseValue::Pointer { object_id, .. } => Ok(Some(object_id.clone())),
312        // Note what is *not* checked: upstream does not require `className` to agree with the
313        // relation's target, and does not reject a missing one. Adding either would narrow a query
314        // it answers.
315        ParseValue::Object(map) => Ok(match map.get("objectId") {
316            Some(ParseValue::String(id)) => Some(id.clone()),
317            _ => None,
318        }),
319        _ => Ok(None),
320    }
321}
322
323/// Does this operand carry the `Pointer` tag?
324///
325/// **Only shorthand equality asks.** The gate is
326/// `query[key].$in || query[key].$ne || query[key].$nin || query[key].__type == 'Pointer'`
327/// (`DatabaseController.js:1084-1090`): the first three are satisfied by the *operator* being
328/// present, and only the fourth, which is the no-operator case, inspects a tag.
329fn is_tagged_pointer(value: &ParseValue) -> bool {
330    match value {
331        ParseValue::Pointer { .. } => true,
332        ParseValue::Object(map) => {
333            matches!(map.get("__type"), Some(ParseValue::String(t)) if t == "Pointer")
334        }
335        _ => false,
336    }
337}
338
339/// Does this comparison satisfy upstream's gate?
340///
341/// ```text
342/// query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin']
343///                || query[key].__type == 'Pointer')
344/// ```
345///
346/// (`DatabaseController.js:1084-1090`.) **Every term is a truthiness test**, and the third one has
347/// teeth because `$ne`'s operand is arbitrary: `$ne: null`, `$ne: false`, `$ne: 0` and `$ne: ""`
348/// are all falsy, so the gate fails and the whole constraint resolves to no owners. Treating `$ne`
349/// as satisfied merely by being present made those four return **every** owner, where upstream
350/// returns none. Measured against a running server at the pin.
351///
352/// `$in` and `$nin` always satisfy it, because the parser guarantees an array and every array is
353/// truthy in JavaScript, the empty one included.
354fn satisfies_gate(comparison: &Comparison) -> bool {
355    match comparison {
356        Comparison::Equal(v) => is_tagged_pointer(v),
357        Comparison::In(_) | Comparison::NotIn(_) => true,
358        Comparison::NotEqual(v) => js_number::is_truthy(v),
359        _ => false,
360    }
361}
362
363/// Read **every constraint on one `Relation`-typed field**, as one group.
364///
365/// **The group is the unit, not the comparison, and that is what makes the gate expressible.**
366/// Upstream tests `query[key]`, the entire operator document, and then iterates its keys
367/// (`DatabaseController.js:1084-1112`). So `{"$ne": false, "$in": [<pointer>]}` passes on the `$in`
368/// and still processes the `$ne`, which contributes nothing because its operand names no id.
369/// Evaluating the gate one comparison at a time cannot express that: it either drops the `$in`
370/// along with the `$ne` or keeps the `$ne` along with the `$in`, and the second is what returned
371/// every owner for a falsy `$ne` alone.
372///
373/// Returns one entry per operator upstream would build a query for, which is why it is a `Vec`:
374/// `{"$in": [a], "$nin": [b]}` is an inclusion *and* an exclusion, applied independently.
375///
376/// **The tag requirement belongs to shorthand equality alone.** Requiring it everywhere silently
377/// drops the ids out of a `$nin`, and an empty exclusion list excludes nobody, so the query returns
378/// the owners it was told to remove.
379///
380/// ACL and CLP still apply to whatever this produces, so getting it wrong widens a result set
381/// rather than bypassing authorization. Widening is still wrong.
382pub fn relation_constraints_for(
383    comparisons: &[Comparison],
384) -> Result<Vec<RelationConstraint>, ParseError> {
385    fn ids(values: &[ParseValue]) -> Result<Vec<String>, ParseError> {
386        let mut out = Vec::new();
387        for value in values {
388            if let Some(id) = object_id_of(value)? {
389                out.push(id);
390            }
391        }
392        Ok(out)
393    }
394    // The gate fails: `queries = [{isNegation: false, relatedIds: []}]`, which resolves to no
395    // owners and therefore to an empty result. Nothing is extracted, so nothing is refused: a bare
396    // `{"$ne": null}` is an empty result upstream and here, not an error.
397    if !comparisons.iter().any(satisfies_gate) {
398        return Ok(vec![RelationConstraint::OwnersOf(Vec::new())]);
399    }
400    let mut out = Vec::new();
401    for comparison in comparisons {
402        out.push(match comparison {
403            // The gate's fourth term, and the only one that reads a tag.
404            Comparison::Equal(v) if is_tagged_pointer(v) => {
405                RelationConstraint::OwnersOf(object_id_of(v)?.into_iter().collect())
406            }
407            Comparison::In(values) => RelationConstraint::OwnersOf(ids(values)?),
408            // An empty list is upstream's answer too, and it is the right one: `$nin` against no
409            // ids excludes nothing. It arises from `relatedIds` holding only `undefined`, which
410            // `owningIds` resolves to no owners.
411            Comparison::NotIn(values) => RelationConstraint::NotOwnersOf(ids(values)?),
412            // A falsy `$ne` that rode in on a sibling's gate names no id, so it excludes nothing.
413            // A **null** one is refused, because that is where upstream throws.
414            Comparison::NotEqual(v) => {
415                RelationConstraint::NotOwnersOf(object_id_of(v)?.into_iter().collect())
416            }
417            // Upstream's `else { return; }`: a key it does not handle yields `undefined`, and
418            // `if (!q) return` drops it before any query runs. `$eq` lands here, having no case of
419            // its own, and so does every operator that is not one of the four.
420            _ => continue,
421        });
422    }
423    Ok(out)
424}
425
426/// Intersect an `objectId $in` into a query. `addInObjectIdsIds`
427/// (`DatabaseController.js:1310-1345`).
428///
429/// The intersection is the point. Two separate `objectId` constraints cannot be conjoined by the
430/// Mongo lowering (a second `$in` would overwrite the first, and an `$eq` beside an `$in` is a
431/// conflict), so the existing constraints are collected and folded in here instead.
432pub fn add_in_object_ids(query: &mut Query, ids: &[String]) {
433    let mut sets: Vec<Vec<String>> = Vec::new();
434    query.clauses.retain(|clause| match clause {
435        Clause::Field(Constraint { field, comparison }) if field == "objectId" => {
436            match comparison {
437                Comparison::Equal(ParseValue::String(id)) => {
438                    sets.push(vec![id.clone()]);
439                    false
440                }
441                Comparison::In(values) => {
442                    sets.push(
443                        values
444                            .iter()
445                            .filter_map(|v| match v {
446                                ParseValue::String(s) => Some(s.clone()),
447                                _ => None,
448                            })
449                            .collect(),
450                    );
451                    false
452                }
453                _ => true,
454            }
455        }
456        _ => true,
457    });
458    sets.push(ids.to_vec());
459
460    let mut intersection: Vec<String> = Vec::new();
461    if let Some((first, rest)) = sets.split_first() {
462        for id in first {
463            if !intersection.contains(id) && rest.iter().all(|set| set.contains(id)) {
464                intersection.push(id.clone());
465            }
466        }
467    }
468    query.push_constraint(Constraint::one_of(
469        "objectId",
470        intersection.into_iter().map(ParseValue::String).collect(),
471    ));
472}
473
474/// Union an `objectId $nin` into a query. `addNotInObjectIdsIds`
475/// (`DatabaseController.js:1347-1372`).
476pub fn add_not_in_object_ids(query: &mut Query, ids: &[String]) {
477    let mut union: Vec<String> = Vec::new();
478    query.clauses.retain(|clause| match clause {
479        Clause::Field(Constraint {
480            field,
481            comparison: Comparison::NotIn(values),
482        }) if field == "objectId" => {
483            for v in values {
484                if let ParseValue::String(s) = v {
485                    if !union.contains(s) {
486                        union.push(s.clone());
487                    }
488                }
489            }
490            false
491        }
492        _ => true,
493    });
494    for id in ids {
495        if !union.contains(id) {
496            union.push(id.clone());
497        }
498    }
499    query.push_constraint(Constraint {
500        field: "objectId".to_string(),
501        comparison: Comparison::NotIn(union.into_iter().map(ParseValue::String).collect()),
502    });
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use parse_rust_core::{op::OpPath, FieldWrite};
509
510    fn body(json: &str) -> crate::WriteBody {
511        crate::decode_write_body(
512            &serde_json::from_str(json).expect("test literal"),
513            OpPath::Update,
514        )
515        .expect("decode")
516    }
517
518    #[test]
519    fn relation_ops_are_stripped_out_of_the_write() {
520        let mut b = body(
521            r#"{
522                "name":"admins",
523                "users":{"__op":"AddRelation","objects":[
524                    {"__type":"Pointer","className":"_User","objectId":"u1"},
525                    {"__type":"Pointer","className":"_User","objectId":"u2"}
526                ]}
527            }"#,
528        );
529        let ops = collect_relation_updates(&mut b);
530        assert!(b.contains_key("name"));
531        assert!(
532            !b.contains_key("users"),
533            "a Relation field has no column, so it must not reach the row write"
534        );
535        assert_eq!(ops.len(), 1);
536        assert_eq!(ops[0].kind, RelationOpKind::Add);
537        assert_eq!(ops[0].related_ids, vec!["u1", "u2"]);
538    }
539
540    #[test]
541    fn a_batch_of_relation_ops_is_stripped_whole() {
542        let mut b = body(
543            r#"{"users":{"__op":"Batch","ops":[
544                {"__op":"AddRelation","objects":[{"__type":"Pointer","className":"_User","objectId":"u1"}]},
545                {"__op":"RemoveRelation","objects":[{"__type":"Pointer","className":"_User","objectId":"u2"}]}
546            ]}}"#,
547        );
548        let ops = collect_relation_updates(&mut b);
549        assert!(b.is_empty());
550        assert_eq!(ops.len(), 2);
551        assert_eq!(ops[0].kind, RelationOpKind::Add);
552        assert_eq!(ops[1].kind, RelationOpKind::Remove);
553    }
554
555    #[test]
556    fn a_non_relation_op_is_left_alone() {
557        let mut b = body(r#"{"views":{"__op":"Increment","amount":1}}"#);
558        assert!(collect_relation_updates(&mut b).is_empty());
559        assert!(matches!(b.get("views"), Some(FieldWrite::Op(_))));
560    }
561
562    /// **Both spellings of a pointer operand, and the gate the group has to satisfy.**
563    ///
564    /// A parsed constraint carries the raw object: query operands are no longer decoded before the
565    /// schema is known, since which envelopes count depends on the field. A constraint the ACL and
566    /// CLP paths build in Rust carries a real `Pointer`.
567    #[test]
568    fn relation_field_constraints_map_to_the_reverse_join() {
569        let decoded = ParseValue::Pointer {
570            class_name: "_User".into(),
571            object_id: "u1".into(),
572        };
573        let mut raw = ParseMap::new();
574        raw.insert("__type".into(), ParseValue::String("Pointer".into()));
575        raw.insert("className".into(), ParseValue::String("_User".into()));
576        raw.insert("objectId".into(), ParseValue::String("u1".into()));
577        // An extra key an envelope does not declare, which the raw form keeps and which must not
578        // stop the objectId being read: upstream reads the key off the object and checks nothing
579        // else.
580        raw.insert("extra".into(), ParseValue::Number(7.0));
581        let raw = ParseValue::Object(raw);
582
583        let one = |c: Comparison| {
584            let out = relation_constraints_for(&[c]).expect("accepted");
585            assert_eq!(out.len(), 1, "{out:?}");
586            out.into_iter().next().unwrap()
587        };
588
589        for pointer in [decoded, raw] {
590            assert!(matches!(
591                one(Comparison::Equal(pointer.clone())),
592                RelationConstraint::OwnersOf(ids) if ids == ["u1"]
593            ));
594            assert!(matches!(
595                one(Comparison::In(vec![pointer.clone()])),
596                RelationConstraint::OwnersOf(ids) if ids == ["u1"]
597            ));
598            assert!(matches!(
599                one(Comparison::NotIn(vec![pointer.clone()])),
600                RelationConstraint::NotOwnersOf(ids) if ids == ["u1"]
601            ));
602            assert!(matches!(
603                one(Comparison::NotEqual(pointer)),
604                RelationConstraint::NotOwnersOf(ids) if ids == ["u1"]
605            ));
606        }
607
608        // Anything else fails upstream's gate and yields no owners, which is an empty result
609        // rather than an unconstrained one.
610        assert!(matches!(
611            one(Comparison::Exists(true)),
612            RelationConstraint::OwnersOf(ids) if ids.is_empty()
613        ));
614        // Shorthand equality is the one form that needs the tag, because it is the one term of
615        // upstream's gate that inspects a value rather than a key.
616        let untagged = {
617            let mut m = ParseMap::new();
618            m.insert("objectId".into(), ParseValue::String("u1".into()));
619            ParseValue::Object(m)
620        };
621        assert!(matches!(
622            one(Comparison::Equal(untagged.clone())),
623            RelationConstraint::OwnersOf(ids) if ids.is_empty()
624        ));
625
626        // **And the three operator forms must not need it.** An operator satisfies the gate by
627        // being present, so the id is read whatever the operand is tagged. Requiring the tag here
628        // dropped it, and an empty `$nin` excludes nobody: the query then returns the very owners
629        // it was told to remove.
630        assert!(matches!(
631            one(Comparison::NotIn(vec![untagged.clone()])),
632            RelationConstraint::NotOwnersOf(ids) if ids == ["u1"]
633        ));
634        assert!(matches!(
635            one(Comparison::In(vec![untagged.clone()])),
636            RelationConstraint::OwnersOf(ids) if ids == ["u1"]
637        ));
638        assert!(matches!(
639            one(Comparison::NotEqual(untagged)),
640            RelationConstraint::NotOwnersOf(ids) if ids == ["u1"]
641        ));
642    }
643
644    /// **A falsy `$ne` fails the gate, and failing the gate means no owners rather than no
645    /// exclusion.**
646    ///
647    /// Measured against a running server at the pin: `$ne` with `null`, `false`, `0` or `""`
648    /// returns nothing at all. Producing `NotOwnersOf([])` for them instead excludes nobody, so
649    /// every owner comes back. ACL and CLP still apply on top, so this widens a result set rather
650    /// than bypassing authorization, and widening is still the wrong direction.
651    #[test]
652    fn a_falsy_ne_fails_the_gate_and_returns_no_owners() {
653        for falsy in [
654            ParseValue::Null,
655            ParseValue::Bool(false),
656            ParseValue::Number(0.0),
657            ParseValue::String(String::new()),
658        ] {
659            let out =
660                relation_constraints_for(&[Comparison::NotEqual(falsy.clone())]).expect("accepted");
661            assert!(
662                matches!(out.as_slice(), [RelationConstraint::OwnersOf(ids)] if ids.is_empty()),
663                "{falsy:?} must fail the gate, got {out:?}"
664            );
665        }
666        // A truthy non-object operand passes the gate and names no id, so it excludes nothing.
667        // That is upstream's `undefined` reaching `owningIds`, and it returns every owner.
668        let out = relation_constraints_for(&[Comparison::NotEqual(ParseValue::Number(7.0))])
669            .expect("accepted");
670        assert!(
671            matches!(out.as_slice(), [RelationConstraint::NotOwnersOf(ids)] if ids.is_empty()),
672            "{out:?}"
673        );
674    }
675
676    /// **A `null` operand is refused rather than skipped.**
677    ///
678    /// It is the one value upstream cannot read `.objectId` from, so it raises an uncaught
679    /// `TypeError` and the request 500s. Skipping it is the one answer that must not be given:
680    /// a dropped element of a `$nin` leaves an empty exclusion list, which excludes nobody, so the
681    /// query returns every otherwise-readable row where upstream returns none.
682    #[test]
683    fn a_null_operand_is_refused_rather_than_erased() {
684        for comparison in [
685            Comparison::In(vec![ParseValue::Null]),
686            Comparison::NotIn(vec![ParseValue::Null]),
687            Comparison::In(vec![
688                ParseValue::Pointer {
689                    class_name: "_User".into(),
690                    object_id: "u1".into(),
691                },
692                ParseValue::Null,
693            ]),
694        ] {
695            let err = relation_constraints_for(std::slice::from_ref(&comparison))
696                .expect_err("a null operand is refused");
697            assert_eq!(
698                err.message, "cannot use null in a constraint on a Relation field",
699                "{comparison:?}"
700            );
701        }
702
703        // A `$ne: null` riding in on a truthy sibling is extracted, so it is refused too. This is
704        // the case that 500s upstream.
705        let err = relation_constraints_for(&[
706            Comparison::NotEqual(ParseValue::Null),
707            Comparison::In(Vec::new()),
708        ])
709        .expect_err("refused");
710        assert_eq!(
711            err.message,
712            "cannot use null in a constraint on a Relation field"
713        );
714
715        // **A bare `{"$ne": null}` is not refused**, because it fails the gate and nothing is ever
716        // extracted. Upstream answers it with an empty result rather than an error, and so does
717        // this.
718        let out = relation_constraints_for(&[Comparison::NotEqual(ParseValue::Null)])
719            .expect("the gate fails before anything is read");
720        assert!(
721            matches!(out.as_slice(), [RelationConstraint::OwnersOf(ids)] if ids.is_empty()),
722            "{out:?}"
723        );
724
725        // A non-null operand that names no id is still harmless, which is what makes the refusal
726        // specific to `null` rather than to "not a pointer".
727        let out = relation_constraints_for(&[Comparison::In(vec![ParseValue::Number(7.0)])])
728            .expect("accepted");
729        assert!(
730            matches!(out.as_slice(), [RelationConstraint::OwnersOf(ids)] if ids.is_empty()),
731            "{out:?}"
732        );
733    }
734
735    /// **The gate is evaluated over the whole operator document, which is why the group is the
736    /// unit.**
737    ///
738    /// `{"$ne": false, "$in": [<pointer>]}` passes on the `$in` and still processes the `$ne`.
739    /// Deciding per comparison cannot express that: it either drops the `$in` with the `$ne`, or
740    /// keeps the `$ne` with the `$in` and so widens the falsy case above.
741    #[test]
742    fn a_truthy_sibling_carries_a_falsy_ne_through_the_gate() {
743        let pointer = ParseValue::Pointer {
744            class_name: "_User".into(),
745            object_id: "u1".into(),
746        };
747        let out = relation_constraints_for(&[
748            Comparison::NotEqual(ParseValue::Bool(false)),
749            Comparison::In(vec![pointer]),
750        ])
751        .expect("accepted");
752        // Two reads, in the order the operators were given: the `$ne` excludes nothing because it
753        // names no id, and the `$in` includes u1's owners.
754        assert!(
755            matches!(
756                out.as_slice(),
757                [RelationConstraint::NotOwnersOf(none), RelationConstraint::OwnersOf(one)]
758                    if none.is_empty() && one.as_slice() == ["u1"]
759            ),
760            "{out:?}"
761        );
762    }
763    #[test]
764    fn object_id_constraints_intersect_rather_than_stack() {
765        let mut q = Query::from_constraints(vec![Constraint::equal(
766            "objectId",
767            ParseValue::String("a".into()),
768        )]);
769        add_in_object_ids(&mut q, &["a".to_string(), "b".to_string()]);
770        assert_eq!(q.clauses.len(), 1, "the original constraint is folded in");
771        match &q.clauses[0] {
772            Clause::Field(Constraint {
773                comparison: Comparison::In(values),
774                ..
775            }) => assert_eq!(values.len(), 1),
776            other => panic!("expected an In, got {other:?}"),
777        }
778    }
779
780    #[test]
781    fn a_denied_related_to_intersects_to_nothing() {
782        let mut q = Query::new();
783        add_in_object_ids(&mut q, &[]);
784        match &q.clauses[0] {
785            Clause::Field(Constraint {
786                comparison: Comparison::In(values),
787                ..
788            }) => assert!(values.is_empty()),
789            other => panic!("expected an empty In, got {other:?}"),
790        }
791    }
792
793    #[test]
794    fn not_in_object_ids_unions() {
795        let mut q = Query::from_constraints(vec![Constraint {
796            field: "objectId".into(),
797            comparison: Comparison::NotIn(vec![ParseValue::String("a".into())]),
798        }]);
799        add_not_in_object_ids(&mut q, &["b".to_string(), "a".to_string()]);
800        assert_eq!(q.clauses.len(), 1);
801        match &q.clauses[0] {
802            Clause::Field(Constraint {
803                comparison: Comparison::NotIn(values),
804                ..
805            }) => assert_eq!(values.len(), 2),
806            other => panic!("expected a NotIn, got {other:?}"),
807        }
808    }
809
810    #[tokio::test]
811    async fn the_protected_key_check_precedes_the_read() {
812        let e = authorize_related_to(
813            "_Role",
814            "users",
815            &["users".to_string()],
816            ErrorDetail::Disclosed,
817            || async { panic!("the owning object must not be read once the key is refused") },
818        )
819        .await
820        .unwrap_err();
821        assert_eq!(e.code, ErrorCode::OperationForbidden);
822        assert_eq!(
823            e.message,
824            "This user is not allowed to query users on class _Role"
825        );
826
827        // The default regime says only that it was refused, and still does not read the owner.
828        let withheld = authorize_related_to(
829            "_Role",
830            "users",
831            &["users".to_string()],
832            ErrorDetail::Withheld,
833            || async { panic!("the owning object must not be read once the key is refused") },
834        )
835        .await
836        .unwrap_err();
837        assert_eq!(withheld.code, ErrorCode::OperationForbidden);
838        assert_eq!(withheld.message, "Permission denied");
839    }
840}