parse_rust_core/clp.rs
1//! Class-level permissions: the model, and nothing that evaluates it.
2//!
3//! CLP lives here rather than in `parse-rust-schema` for the same reason [`crate::acl`] does. It is
4//! an authorization data model with a JSON encoding and no I/O, and both the schema crate (which
5//! validates it) and the REST crate (which enforces it) need the type. Validation messages and
6//! evaluation both live above this crate.
7//!
8//! Three shapes here are load-bearing, and each one fails as a security defect rather than as a
9//! test failure if it is modelled the obvious way instead of upstream's way.
10//!
11//! **CLP is default-open.** `testPermissions` allows when `classPermissions[operation]` is
12//! *falsy* (`SchemaController.js:1365-1382`): an absent operation entry means unrestricted, not
13//! denied. That is why [`ClassLevelPermissions::op`] returns `Option<&OpPerm>` with `None`
14//! meaning unrestricted, and why **[`OpPerm`] deliberately has no `Default` impl**. A `Default`
15//! would be an empty entity set, which is deny-all, so a refactor that reached for
16//! `unwrap_or_default()` would invert the rule and lock every existing database out.
17//!
18//! **The two entity grammars are not interchangeable.** Operations and `addField` accept
19//! `pointerFields`, `*`, `requiresAuthentication`, `role:<name>` and an objectId
20//! (`validatePermissionKey`, `SchemaController.js:218-235`). `protectedFields` accepts
21//! `userField:<name>`, `*`, `authenticated`, `role:<name>` and an objectId
22//! (`validateProtectedFieldsKey`, `:237-254`). One shared enum gets this wrong in both
23//! directions, so there are two: [`OpEntity`] and [`PfEntity`].
24//!
25//! **The raw block is kept verbatim.** parse-rust must never rewrite a key it does not
26//! understand back out of `_metadata.class_permissions`, because a parse-server node reading the
27//! same database would see the key vanish. [`ClassLevelPermissions::raw`] is what gets written;
28//! the parsed view is only ever read.
29
30use indexmap::IndexMap;
31
32use crate::value::{ParseMap, ParseValue};
33
34/// The seven operations a CLP can restrict.
35///
36/// `addField` is one of them, and it is checked on every non-master write that introduces a key
37/// the schema does not have (`DatabaseController.js:970-998`).
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum Operation {
40 Find,
41 Count,
42 Get,
43 Create,
44 Update,
45 Delete,
46 AddField,
47}
48
49impl Operation {
50 /// The key as it appears in the CLP object, and in
51 /// `Permission denied for action <op> on class <Class>.`
52 pub fn as_key(self) -> &'static str {
53 match self {
54 Operation::Find => "find",
55 Operation::Count => "count",
56 Operation::Get => "get",
57 Operation::Create => "create",
58 Operation::Update => "update",
59 Operation::Delete => "delete",
60 Operation::AddField => "addField",
61 }
62 }
63
64 /// Which of the two class-wide pointer-field arrays applies to this operation.
65 ///
66 /// `permissionField = ['get','find','count'].indexOf(operation) > -1 ? 'readUserFields' :
67 /// 'writeUserFields'` (`SchemaController.js:1425-1429`). Note `addField` falls on the write
68 /// side, which is what makes the create lockdown reachable.
69 pub fn user_fields_key(self) -> UserFieldsKey {
70 match self {
71 Operation::Get | Operation::Find | Operation::Count => UserFieldsKey::Read,
72 _ => UserFieldsKey::Write,
73 }
74 }
75
76 pub const ALL: [Operation; 7] = [
77 Operation::Find,
78 Operation::Count,
79 Operation::Get,
80 Operation::Create,
81 Operation::Update,
82 Operation::Delete,
83 Operation::AddField,
84 ];
85
86 pub fn from_key(key: &str) -> Option<Operation> {
87 Operation::ALL.into_iter().find(|op| op.as_key() == key)
88 }
89}
90
91/// Which class-wide pointer-field array an operation consults.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum UserFieldsKey {
94 Read,
95 Write,
96}
97
98/// An entity key inside an operation's permission object.
99///
100/// `pointerFields` is not here: it is a sibling key whose value is an array of field names rather
101/// than an entity granted `true`, and conflating the two is how a permission object stops being a
102/// map from principal to grant.
103#[derive(Debug, Clone, PartialEq, Eq, Hash)]
104pub enum OpEntity {
105 /// `"*"`
106 Public,
107 /// `"requiresAuthentication"`. Not a principal: a predicate over the caller.
108 RequiresAuthentication,
109 /// `"role:<name>"`, stored without the prefix.
110 Role(String),
111 /// An objectId.
112 User(String),
113}
114
115impl OpEntity {
116 /// Total, so a `role:`-prefixed string can never end up in the `User` arm.
117 ///
118 /// That totality is the mitigation for the `role:` objectId collision: upstream has to guard
119 /// it at two call sites with an explicit `startsWith` check (`Auth.js:195`, `:237`) precisely
120 /// because its entity namespace is one flat untyped string space.
121 pub fn parse(key: &str) -> Self {
122 if key == "*" {
123 OpEntity::Public
124 } else if key == "requiresAuthentication" {
125 OpEntity::RequiresAuthentication
126 } else if let Some(name) = key.strip_prefix("role:") {
127 OpEntity::Role(name.to_string())
128 } else {
129 OpEntity::User(key.to_string())
130 }
131 }
132
133 pub fn as_key(&self) -> String {
134 match self {
135 OpEntity::Public => "*".to_string(),
136 OpEntity::RequiresAuthentication => "requiresAuthentication".to_string(),
137 OpEntity::Role(name) => format!("role:{name}"),
138 OpEntity::User(id) => id.clone(),
139 }
140 }
141}
142
143/// An entity key inside `protectedFields`.
144///
145/// Note what is *not* shared with [`OpEntity`]: there is no `requiresAuthentication` (the
146/// spelling here is `authenticated`) and no `pointerFields` (the spelling here is
147/// `userField:<name>`). Upstream validates the two with two different functions and two different
148/// key sets.
149#[derive(Debug, Clone, PartialEq, Eq, Hash)]
150pub enum PfEntity {
151 /// `"*"`. Always applicable.
152 Public,
153 /// `"authenticated"`. Applicable only when the caller is a logged-in user.
154 Authenticated,
155 /// `"userField:<name>"`. Applicable when the named field on the *row* points at the caller,
156 /// which cannot be known until the row has been read.
157 UserField(String),
158 /// `"role:<name>"`, stored without the prefix.
159 Role(String),
160 /// An objectId.
161 User(String),
162}
163
164impl PfEntity {
165 pub fn parse(key: &str) -> Self {
166 if key == "*" {
167 PfEntity::Public
168 } else if key == "authenticated" {
169 PfEntity::Authenticated
170 } else if let Some(field) = key.strip_prefix("userField:") {
171 PfEntity::UserField(field.to_string())
172 } else if let Some(name) = key.strip_prefix("role:") {
173 PfEntity::Role(name.to_string())
174 } else {
175 PfEntity::User(key.to_string())
176 }
177 }
178
179 pub fn as_key(&self) -> String {
180 match self {
181 PfEntity::Public => "*".to_string(),
182 PfEntity::Authenticated => "authenticated".to_string(),
183 PfEntity::UserField(f) => format!("userField:{f}"),
184 PfEntity::Role(name) => format!("role:{name}"),
185 PfEntity::User(id) => id.clone(),
186 }
187 }
188}
189
190/// One operation's permission object.
191///
192/// **No `Default` impl, on purpose.** See the module note: an empty `OpPerm` is deny-all, and
193/// absent is unrestricted, so the two must never be reachable from one another by accident.
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct OpPerm {
196 /// Entities granted the operation. Only a literal `true` counts upstream
197 /// (`SchemaController.js:367-396`); `false`, `0` and `"true"` are all `INVALID_JSON` at
198 /// validation, so anything reaching here is a grant.
199 pub entities: Vec<OpEntity>,
200 /// `pointerFields`: the caller must be the value of one of these fields on the row.
201 pub pointer_fields: Vec<String>,
202}
203
204impl OpPerm {
205 // No `Default` impl, and the lint asking for one is wrong here. An empty `OpPerm` is
206 // deny-all, while an *absent* one is unrestricted, so a `Default` would put deny-all one
207 // `unwrap_or_default()` away from every call site that means unrestricted. That is the
208 // inversion this module exists to prevent.
209 #[allow(clippy::new_without_default)]
210 pub fn new() -> Self {
211 Self {
212 entities: Vec::new(),
213 pointer_fields: Vec::new(),
214 }
215 }
216
217 pub fn grants(&self, entity: &OpEntity) -> bool {
218 self.entities.contains(entity)
219 }
220}
221
222/// Class-level permissions.
223///
224/// Constructed from the stored `_metadata.class_permissions` object or from a
225/// `POST`/`PUT /schemas` body. Both paths keep [`Self::raw`] verbatim so that writing the block
226/// back cannot drop a key parse-rust does not model.
227///
228/// No `PartialEq`, because [`Self::raw`] holds [`ParseValue`]s and a derived comparison would
229/// inherit that type's float hazards. Compare the rendered JSON if two blocks need comparing.
230#[derive(Debug, Clone)]
231pub struct ClassLevelPermissions {
232 raw: ParseMap,
233 ops: Vec<(Operation, OpPerm)>,
234 protected_fields: IndexMap<PfEntity, Vec<String>>,
235 read_user_fields: Vec<String>,
236 write_user_fields: Vec<String>,
237}
238
239impl ClassLevelPermissions {
240 /// Parse a CLP block. Unknown keys are preserved in [`Self::raw`] and otherwise ignored;
241 /// rejecting them is the schema API's job, not the model's, because a block already in the
242 /// database has to be readable even if it would fail validation today.
243 pub fn from_map(raw: ParseMap) -> Self {
244 let mut ops = Vec::new();
245 for op in Operation::ALL {
246 match raw.get(op.as_key()) {
247 Some(ParseValue::Object(entry)) => ops.push((op, parse_op_perm(entry))),
248 // A present but malformed entry denies. `testPermissions` allows only when
249 // `classPermissions[operation]` is *falsy* (`SchemaController.js:1365-1382`), so
250 // `{"find": true}`, `{"find": "x"}` and `{"find": []}` all deny upstream: the
251 // value is truthy, so the short-circuit does not fire, and none of the lookups
252 // that follow finds a grant on a non-object.
253 //
254 // Recording an empty `OpPerm` rather than skipping the key is what reproduces
255 // that. Skipping would make the entry absent, which is unrestricted, and the
256 // failure direction there is open. Not reachable through `PUT /schemas`, which
257 // refuses a non-object operation value, but a block written straight into
258 // `_SCHEMA` has to be read the way parse-server reads it.
259 Some(other) if is_js_truthy(other) => ops.push((op, OpPerm::new())),
260 // Falsy, so `!classPermissions[operation]` holds and the operation is
261 // unrestricted, exactly as an absent key is.
262 _ => {}
263 }
264 }
265
266 let mut protected_fields = IndexMap::new();
267 if let Some(ParseValue::Object(pf)) = raw.get("protectedFields") {
268 for (key, value) in pf {
269 if let ParseValue::Array(items) = value {
270 protected_fields.insert(PfEntity::parse(key), string_array(items));
271 }
272 }
273 }
274
275 Self {
276 read_user_fields: raw
277 .get("readUserFields")
278 .map(string_array_of)
279 .unwrap_or_default(),
280 write_user_fields: raw
281 .get("writeUserFields")
282 .map(string_array_of)
283 .unwrap_or_default(),
284 ops,
285 protected_fields,
286 raw,
287 }
288 }
289
290 /// The block exactly as it will be stored. Never derived from the parsed view.
291 pub fn raw(&self) -> &ParseMap {
292 &self.raw
293 }
294
295 /// The permission object for one operation.
296 ///
297 /// **`None` means unrestricted, not denied.** Every caller has to spell that out; the reason
298 /// it cannot be `unwrap_or_default()` is the whole point of this module.
299 pub fn op(&self, operation: Operation) -> Option<&OpPerm> {
300 self.ops
301 .iter()
302 .find(|(o, _)| *o == operation)
303 .map(|(_, p)| p)
304 }
305
306 pub fn protected_fields(&self) -> &IndexMap<PfEntity, Vec<String>> {
307 &self.protected_fields
308 }
309
310 /// The class-wide pointer-field array for one operation, per
311 /// `SchemaController.js:1425-1429`.
312 pub fn user_fields(&self, operation: Operation) -> &[String] {
313 match operation.user_fields_key() {
314 UserFieldsKey::Read => &self.read_user_fields,
315 UserFieldsKey::Write => &self.write_user_fields,
316 }
317 }
318
319 pub fn read_user_fields(&self) -> &[String] {
320 &self.read_user_fields
321 }
322
323 pub fn write_user_fields(&self) -> &[String] {
324 &self.write_user_fields
325 }
326
327 /// The class's declared default ACL, if it has one that upstream would stamp on a create.
328 ///
329 /// `None` covers three cases that upstream's condition collapses (`RestWrite.js:379-384`):
330 /// the `ACL` key is absent; its value is falsy, which is `schema?.classLevelPermissions?.ACL`
331 /// failing its own truthiness test; or it is exactly the public ACL, which upstream skips
332 /// because stamping `{"*": {"read": true, "write": true}}` on a row would only reproduce what
333 /// an absent ACL already means.
334 ///
335 /// **That last comparison is `JSON.stringify` equality upstream, so it is key-order
336 /// sensitive**, and [`is_the_public_acl`] reproduces the ordering rather than comparing
337 /// structurally. A block whose keys arrived in the other order is *not* the public ACL as far
338 /// as upstream is concerned, and it gets stamped.
339 ///
340 /// A truthy non-object is returned rather than filtered: upstream clones and assigns whatever
341 /// it finds, and the resulting `ACL` value is then lowered by the same rule any client-supplied
342 /// one is.
343 pub fn default_acl(&self) -> Option<&ParseValue> {
344 let acl = self.raw.get("ACL")?;
345 if !is_js_truthy(acl) || is_the_public_acl(acl) {
346 return None;
347 }
348 Some(acl)
349 }
350
351 /// Every pointer field that applies to an operation, per-op first then class-wide, deduped.
352 ///
353 /// Order is upstream's (`DatabaseController.js:1749-1764`) and matters, because the clauses
354 /// are composed into an `$or` whose element order is observable in a compiled query.
355 pub fn applicable_pointer_fields(&self, operation: Operation) -> Vec<String> {
356 let mut out: Vec<String> = Vec::new();
357 if let Some(perm) = self.op(operation) {
358 for f in &perm.pointer_fields {
359 if !out.contains(f) {
360 out.push(f.clone());
361 }
362 }
363 }
364 for f in self.user_fields(operation) {
365 if !out.contains(f) {
366 out.push(f.clone());
367 }
368 }
369 out
370 }
371}
372
373/// Is this value what `JSON.stringify` would render as `{"*":{"read":true,"write":true}}`?
374///
375/// The one comparison upstream makes by stringifying both sides
376/// (`RestWrite.js:382-383`), which makes it **sensitive to key order**: a block spelled
377/// `{"*":{"write":true,"read":true}}` stringifies differently and is therefore not the public ACL,
378/// so upstream stamps it onto every new object. The observable result is the same permissions
379/// either way, but the row carries `_rperm` and `_wperm` in one case and neither in the other, and
380/// a mixed fleet has to agree on which.
381///
382/// Written as an ordered structural test rather than by building a JSON string. For this one
383/// literal the two are the same predicate: a value stringifies to it exactly when it is an object
384/// of one key `*` whose value is an object of two keys, `read` then `write`, both `true`. Anything
385/// else, including `{"*":{"read":true,"write":true,"x":1}}` or `read: 1` instead of `read: true`,
386/// renders a different string and is correctly not the public ACL.
387fn is_the_public_acl(value: &ParseValue) -> bool {
388 let ParseValue::Object(entries) = value else {
389 return false;
390 };
391 let mut entries = entries.iter();
392 let (Some(("*", ParseValue::Object(flags))), None) =
393 (entries.next().map(|(k, v)| (k.as_str(), v)), entries.next())
394 else {
395 return false;
396 };
397 let mut flags = flags.iter();
398 matches!(
399 (
400 flags.next().map(|(k, v)| (k.as_str(), v)),
401 flags.next().map(|(k, v)| (k.as_str(), v)),
402 flags.next(),
403 ),
404 (
405 Some(("read", ParseValue::Bool(true))),
406 Some(("write", ParseValue::Bool(true))),
407 None,
408 )
409 )
410}
411
412/// JavaScript truthiness, which is what `!classPermissions[operation]` tests.
413///
414/// The trap is `Array`: an empty array is **truthy** in JavaScript, and empty-is-falsy is the
415/// reflex a Rust reader brings. Everything with a `__type` envelope is an object on the JS side
416/// and therefore truthy too, including empty `Bytes`.
417///
418/// Public because it is not a CLP concern in particular. Any port of an upstream `if (value)` or
419/// `!value` needs it, and re-deriving the rule per call site is how the `Array` trap gets missed.
420pub fn is_js_truthy(value: &ParseValue) -> bool {
421 match value {
422 ParseValue::Null => false,
423 ParseValue::Bool(b) => *b,
424 // `NaN`, `0` and `-0` are the falsy numbers.
425 ParseValue::Number(n) => *n != 0.0 && !n.is_nan(),
426 ParseValue::String(s) => !s.is_empty(),
427 ParseValue::Array(_)
428 | ParseValue::Object(_)
429 | ParseValue::Date(_)
430 | ParseValue::Pointer { .. }
431 | ParseValue::GeoPoint { .. }
432 | ParseValue::Bytes(_)
433 | ParseValue::File { .. }
434 | ParseValue::Polygon(_)
435 | ParseValue::Relation { .. } => true,
436 }
437}
438
439fn parse_op_perm(entry: &ParseMap) -> OpPerm {
440 let mut perm = OpPerm::new();
441 for (key, value) in entry {
442 if key == "pointerFields" {
443 if let ParseValue::Array(items) = value {
444 perm.pointer_fields = string_array(items);
445 }
446 continue;
447 }
448 if matches!(value, ParseValue::Bool(true)) {
449 perm.entities.push(OpEntity::parse(key));
450 }
451 }
452 perm
453}
454
455fn string_array(items: &[ParseValue]) -> Vec<String> {
456 items
457 .iter()
458 .filter_map(|v| match v {
459 ParseValue::String(s) => Some(s.clone()),
460 _ => None,
461 })
462 .collect()
463}
464
465fn string_array_of(value: &ParseValue) -> Vec<String> {
466 match value {
467 ParseValue::Array(items) => string_array(items),
468 _ => Vec::new(),
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 fn clp(json: &str) -> ClassLevelPermissions {
477 let value = crate::decode::classify(
478 serde_json::from_str(json).expect("test literal must be valid JSON"),
479 )
480 .expect("classify");
481 match value {
482 ParseValue::Object(m) => ClassLevelPermissions::from_map(m),
483 _ => panic!("expected an object"),
484 }
485 }
486
487 /// The single most consequential CLP semantic. If this ever asserts "denied", every existing
488 /// database is locked out on upgrade.
489 #[test]
490 fn an_absent_operation_is_unrestricted_not_denied() {
491 let c = clp(r#"{"find":{"*":true}}"#);
492 assert!(c.op(Operation::Find).is_some());
493 assert!(
494 c.op(Operation::Update).is_none(),
495 "absent means unrestricted, and the caller must be forced to say so"
496 );
497 }
498
499 /// The other half of the same rule: present-but-empty is deny-all, and it must not be
500 /// reachable from the absent case.
501 #[test]
502 fn a_present_but_empty_operation_grants_nobody() {
503 let c = clp(r#"{"find":{}}"#);
504 let perm = c.op(Operation::Find).expect("present");
505 assert!(perm.entities.is_empty());
506 assert!(!perm.grants(&OpEntity::Public));
507 }
508
509 /// The fail-open this closes. A block written straight into `_SCHEMA` can carry a non-object
510 /// operation value, and upstream denies on every truthy one, because `testPermissions` only
511 /// short-circuits to allow when the value is falsy.
512 #[test]
513 fn a_truthy_non_object_operation_denies_rather_than_being_absent() {
514 for json in [
515 r#"{"find":true}"#,
516 r#"{"find":"x"}"#,
517 r#"{"find":[]}"#,
518 r#"{"find":["role:A"]}"#,
519 r#"{"find":1}"#,
520 r#"{"find":{"__type":"Date","iso":"2020-01-01T00:00:00.000Z"}}"#,
521 ] {
522 let c = clp(json);
523 let perm = c
524 .op(Operation::Find)
525 .unwrap_or_else(|| panic!("{json} must be present, not absent"));
526 assert!(perm.entities.is_empty(), "{json} must grant nobody");
527 assert!(!perm.grants(&OpEntity::Public), "{json}");
528 }
529 }
530
531 /// The other half, and the reason this cannot just be "anything non-object denies": a falsy
532 /// value is what `!classPermissions[operation]` is testing for, so it is unrestricted.
533 #[test]
534 fn a_falsy_operation_value_is_unrestricted_like_an_absent_key() {
535 for json in [
536 r#"{"find":false}"#,
537 r#"{"find":null}"#,
538 r#"{"find":0}"#,
539 r#"{"find":""}"#,
540 ] {
541 assert!(
542 clp(json).op(Operation::Find).is_none(),
543 "{json} must read as unrestricted"
544 );
545 }
546 }
547
548 /// The trap inside the trap. Rust reads an empty array as empty; JavaScript reads it as
549 /// truthy, and the two answers are opposite permission decisions.
550 #[test]
551 fn js_truthiness_is_not_rust_emptiness() {
552 assert!(is_js_truthy(&ParseValue::Array(Vec::new())));
553 assert!(is_js_truthy(&ParseValue::Bytes(Vec::new())));
554 assert!(is_js_truthy(&ParseValue::String("0".into())));
555 assert!(!is_js_truthy(&ParseValue::String(String::new())));
556 assert!(!is_js_truthy(&ParseValue::Number(0.0)));
557 assert!(!is_js_truthy(&ParseValue::Number(-0.0)));
558 assert!(!is_js_truthy(&ParseValue::Number(f64::NAN)));
559 assert!(is_js_truthy(&ParseValue::Number(-1.0)));
560 }
561
562 #[test]
563 fn only_literal_true_is_a_grant() {
564 let c = clp(r#"{"find":{"*":false,"role:A":true,"abc":0,"def":"true"}}"#);
565 let perm = c.op(Operation::Find).expect("present");
566 assert_eq!(perm.entities, vec![OpEntity::Role("A".into())]);
567 }
568
569 #[test]
570 fn pointer_fields_is_not_an_entity() {
571 let c = clp(r#"{"find":{"pointerFields":["owner"],"*":true}}"#);
572 let perm = c.op(Operation::Find).expect("present");
573 assert_eq!(perm.pointer_fields, vec!["owner".to_string()]);
574 assert_eq!(perm.entities, vec![OpEntity::Public]);
575 }
576
577 #[test]
578 fn the_two_entity_grammars_do_not_overlap() {
579 // `requiresAuthentication` is an operation key and is an ordinary objectId under the
580 // protectedFields grammar; `authenticated` is the reverse.
581 assert_eq!(
582 OpEntity::parse("requiresAuthentication"),
583 OpEntity::RequiresAuthentication
584 );
585 assert_eq!(
586 PfEntity::parse("requiresAuthentication"),
587 PfEntity::User("requiresAuthentication".into())
588 );
589 assert_eq!(PfEntity::parse("authenticated"), PfEntity::Authenticated);
590 assert_eq!(
591 OpEntity::parse("authenticated"),
592 OpEntity::User("authenticated".into())
593 );
594 assert_eq!(
595 PfEntity::parse("userField:owner"),
596 PfEntity::UserField("owner".into())
597 );
598 assert_eq!(
599 OpEntity::parse("userField:owner"),
600 OpEntity::User("userField:owner".into())
601 );
602 }
603
604 /// The mitigation for the `role:` objectId collision: a `role:`-prefixed string cannot land
605 /// in the `User` arm, so no ACL check can be tricked into granting a role.
606 #[test]
607 fn a_role_prefixed_key_can_never_parse_as_a_user() {
608 assert_eq!(
609 OpEntity::parse("role:Admin"),
610 OpEntity::Role("Admin".into())
611 );
612 assert_eq!(
613 PfEntity::parse("role:Admin"),
614 PfEntity::Role("Admin".into())
615 );
616 for key in ["role:Admin", "role:", "role:with:colons"] {
617 assert!(!matches!(OpEntity::parse(key), OpEntity::User(_)), "{key}");
618 assert!(!matches!(PfEntity::parse(key), PfEntity::User(_)), "{key}");
619 }
620 }
621
622 #[test]
623 fn entity_keys_round_trip() {
624 for key in ["*", "requiresAuthentication", "role:A", "abc123"] {
625 assert_eq!(OpEntity::parse(key).as_key(), key);
626 }
627 for key in ["*", "authenticated", "userField:owner", "role:A", "abc123"] {
628 assert_eq!(PfEntity::parse(key).as_key(), key);
629 }
630 }
631
632 #[test]
633 fn user_fields_split_by_operation_and_add_field_is_a_write() {
634 let c = clp(r#"{"readUserFields":["r"],"writeUserFields":["w"]}"#);
635 for op in [Operation::Get, Operation::Find, Operation::Count] {
636 assert_eq!(c.user_fields(op), ["r".to_string()], "{op:?}");
637 }
638 for op in [
639 Operation::Create,
640 Operation::Update,
641 Operation::Delete,
642 Operation::AddField,
643 ] {
644 assert_eq!(c.user_fields(op), ["w".to_string()], "{op:?}");
645 }
646 }
647
648 #[test]
649 fn applicable_pointer_fields_is_per_op_then_class_wide_deduped() {
650 let c = clp(r#"{"find":{"pointerFields":["owner","a"]},"readUserFields":["a","b"]}"#);
651 assert_eq!(
652 c.applicable_pointer_fields(Operation::Find),
653 vec!["owner".to_string(), "a".to_string(), "b".to_string()]
654 );
655 }
656
657 /// The rule that keeps a mixed fleet from losing configuration: a key parse-rust does not
658 /// model survives the round trip.
659 #[test]
660 fn unmodelled_keys_survive_in_the_raw_block() {
661 let c = clp(r#"{"find":{"*":true},"someFutureKey":{"x":1}}"#);
662 assert!(c.raw().contains_key("someFutureKey"));
663 assert!(c.raw().contains_key("find"));
664 }
665
666 #[test]
667 fn protected_fields_parse_per_entity() {
668 let c = clp(r#"{"protectedFields":{"*":["email"],"role:A":["email","phone"]}}"#);
669 assert_eq!(
670 c.protected_fields().get(&PfEntity::Public),
671 Some(&vec!["email".to_string()])
672 );
673 assert_eq!(
674 c.protected_fields()
675 .get(&PfEntity::Role("A".into()))
676 .map(Vec::len),
677 Some(2)
678 );
679 }
680
681 // -----------------------------------------------------------------------------------------
682 // The declared default ACL
683 // -----------------------------------------------------------------------------------------
684
685 #[test]
686 fn a_declared_acl_is_readable_and_an_absent_one_is_none() {
687 assert!(clp(r#"{"find":{"*":true}}"#).default_acl().is_none());
688 let c = clp(r#"{"ACL":{"currentUser":{"read":true,"write":true}}}"#);
689 let ParseValue::Object(acl) = c.default_acl().expect("declared") else {
690 panic!("expected an object");
691 };
692 assert!(acl.contains_key("currentUser"));
693 }
694
695 /// Upstream tests `schema?.classLevelPermissions?.ACL` for truthiness, so a falsy value is not
696 /// a default ACL at all. Returning it instead would reach `lower_acl`, where a falsy value is
697 /// dropped and the row is public anyway, but the two paths differ for `_Role`, whose ACL is a
698 /// required column.
699 #[test]
700 fn a_falsy_declared_acl_is_not_a_default() {
701 for literal in [
702 r#"{"ACL":null}"#,
703 r#"{"ACL":false}"#,
704 r#"{"ACL":0}"#,
705 r#"{"ACL":""}"#,
706 ] {
707 assert!(clp(literal).default_acl().is_none(), "{literal}");
708 }
709 }
710
711 /// The public ACL is skipped, because stamping it would only restate what an absent ACL
712 /// already means: a row every caller can read and write.
713 #[test]
714 fn the_public_acl_is_not_stamped() {
715 assert!(clp(r#"{"ACL":{"*":{"read":true,"write":true}}}"#)
716 .default_acl()
717 .is_none());
718 }
719
720 /// **The comparison is `JSON.stringify` equality upstream and therefore key-order sensitive.**
721 /// Every literal here grants exactly the same permissions as the public ACL, and upstream
722 /// stamps every one of them, because none stringifies to the same bytes. Comparing
723 /// structurally would skip them all and write no ACL columns where parse-server writes two,
724 /// which a mixed fleet reading the same rows can see.
725 #[test]
726 fn a_reordered_or_extended_public_acl_is_still_stamped() {
727 for literal in [
728 r#"{"ACL":{"*":{"write":true,"read":true}}}"#,
729 r#"{"ACL":{"*":{"read":true,"write":true,"delete":true}}}"#,
730 r#"{"ACL":{"*":{"read":true}}}"#,
731 r#"{"ACL":{"*":{"read":true,"write":true},"role:A":{"read":true}}}"#,
732 r#"{"ACL":{"role:A":{"read":true},"*":{"read":true,"write":true}}}"#,
733 ] {
734 assert!(
735 clp(literal).default_acl().is_some(),
736 "{literal} does not stringify to the public ACL and must be stamped"
737 );
738 }
739 }
740}