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}