Skip to main content

parse_rust_rest/
acl.rs

1//! ACL enforcement: the boundary between the `ACL` field a client sees and the `_rperm`/`_wperm`
2//! columns storage holds.
3//!
4//! **The rule that must not be got wrong: absent permission columns mean public.**
5//! `addReadACL` emits `_rperm: {$in: [null, '*', ...acl]}` and `null` in a Mongo `$in` matches a
6//! document where the field is *missing*, which is how a row saved without an ACL stays readable.
7//! Omitting the null silently hides every such row, and there is no error to notice.
8
9use parse_rust_core::{Acl, ParseMap, ParseValue, Permissions, Principal};
10use parse_rust_storage::{Comparison, Constraint};
11
12/// Who a request is acting as, for ACL purposes.
13///
14/// An enum rather than an `Option<String>` so that "no ACL constraint at all" cannot be reached
15/// by forgetting to set a field. `acl === undefined` as a master sentinel is the upstream shape
16/// this deliberately does not copy.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum AclScope {
19    /// Master or maintenance: no ACL constraint is applied at all.
20    Unrestricted,
21    /// A caller acting as nobody in particular.
22    Anonymous,
23    /// A logged-in user. Roles are out of scope for 0.1.0, so a `role:` entry never matches.
24    User { object_id: String },
25}
26
27impl AclScope {
28    /// The principals this caller matches, in upstream's order: `*` first, then the user id.
29    fn principals(&self) -> Vec<ParseValue> {
30        let mut out = vec![
31            // `null` matches a row with no permission column, i.e. a public row.
32            ParseValue::Null,
33            ParseValue::String("*".to_string()),
34        ];
35        if let AclScope::User { object_id } = self {
36            out.push(ParseValue::String(object_id.clone()));
37        }
38        out
39    }
40
41    /// The constraint to add to a read.
42    ///
43    /// `None` for [`AclScope::Unrestricted`], which is the only case where no constraint is
44    /// applied. Returning `Option` makes the master case explicit at every call site instead of
45    /// being the absence of a step.
46    pub fn read_constraint(&self) -> Option<Constraint> {
47        match self {
48            AclScope::Unrestricted => None,
49            _ => Some(Constraint {
50                field: "_rperm".to_string(),
51                comparison: Comparison::In(self.principals()),
52            }),
53        }
54    }
55
56    /// The constraint to add to a write.
57    ///
58    /// Note the asymmetry with reads: `addWriteACL` omits `'*'` from the injected list, because
59    /// `getUserAndRoleACL` already seeds it for every non-master caller. Reproduced rather than
60    /// unified, since the two functions are not symmetric upstream and a caller path that builds
61    /// its own list would behave differently.
62    pub fn write_constraint(&self) -> Option<Constraint> {
63        match self {
64            AclScope::Unrestricted => None,
65            _ => Some(Constraint {
66                field: "_wperm".to_string(),
67                comparison: Comparison::In(self.principals()),
68            }),
69        }
70    }
71}
72
73/// Split an `ACL` field out of a row into the two storage columns.
74///
75/// Returns the row with `ACL` removed and the columns added. A row with no `ACL` gets no columns,
76/// which is what makes it public.
77pub fn lower_acl(mut row: ParseMap) -> ParseMap {
78    let Some(acl_value) = row.shift_remove("ACL") else {
79        return row;
80    };
81    let Some(acl) = acl_from_value(&acl_value) else {
82        return row;
83    };
84    let (rperm, wperm) = acl.to_perms();
85    row.insert(
86        "_rperm".to_string(),
87        ParseValue::Array(rperm.into_iter().map(ParseValue::String).collect()),
88    );
89    row.insert(
90        "_wperm".to_string(),
91        ParseValue::Array(wperm.into_iter().map(ParseValue::String).collect()),
92    );
93    row
94}
95
96/// Rebuild the `ACL` field from the two storage columns, then drop them.
97///
98/// Reproduces `untransformObjectACL` exactly, including that both columns absent produce **no
99/// `ACL` key at all** rather than `null` or `{}`.
100pub fn raise_acl(mut row: ParseMap) -> ParseMap {
101    let rperm = take_string_array(&mut row, "_rperm");
102    let wperm = take_string_array(&mut row, "_wperm");
103
104    let Some(acl) = Acl::from_perms(rperm.as_deref(), wperm.as_deref()) else {
105        return row;
106    };
107
108    let mut map = ParseMap::new();
109    for (principal, perms) in acl.iter() {
110        if perms.is_empty() {
111            continue;
112        }
113        let mut entry = ParseMap::new();
114        // Only true flags are emitted. UPSTREAM-QUIRK, see `parse_rust_core::acl`.
115        if perms.read {
116            entry.insert("read".to_string(), ParseValue::Bool(true));
117        }
118        if perms.write {
119            entry.insert("write".to_string(), ParseValue::Bool(true));
120        }
121        map.insert(principal.as_key(), ParseValue::Object(entry));
122    }
123    row.insert("ACL".to_string(), ParseValue::Object(map));
124    row
125}
126
127fn take_string_array(row: &mut ParseMap, key: &str) -> Option<Vec<String>> {
128    match row.shift_remove(key) {
129        Some(ParseValue::Array(items)) => Some(
130            items
131                .into_iter()
132                .filter_map(|v| match v {
133                    ParseValue::String(s) => Some(s),
134                    _ => None,
135                })
136                .collect(),
137        ),
138        _ => None,
139    }
140}
141
142/// Read a client-supplied `ACL` value.
143fn acl_from_value(value: &ParseValue) -> Option<Acl> {
144    let ParseValue::Object(map) = value else {
145        return None;
146    };
147    let mut acl = Acl::new();
148    for (key, entry) in map {
149        let ParseValue::Object(flags) = entry else {
150            continue;
151        };
152        let flag = |name: &str| matches!(flags.get(name), Some(ParseValue::Bool(true)));
153        acl.set(
154            Principal::parse(key),
155            Permissions {
156                read: flag("read"),
157                write: flag("write"),
158            },
159        );
160    }
161    Some(acl)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
169        let mut m = ParseMap::new();
170        for (k, v) in pairs {
171            m.insert(k.to_string(), v);
172        }
173        m
174    }
175
176    /// The single most important assertion in this module.
177    #[test]
178    fn the_read_constraint_includes_null_so_public_rows_stay_visible() {
179        let c = AclScope::Anonymous
180            .read_constraint()
181            .expect("anonymous is constrained");
182        assert_eq!(c.field, "_rperm");
183        match c.comparison {
184            Comparison::In(values) => {
185                assert!(
186                    values.iter().any(|v| matches!(v, ParseValue::Null)),
187                    "null must be in the list, or every row saved without an ACL becomes invisible"
188                );
189                assert!(values
190                    .iter()
191                    .any(|v| matches!(v, ParseValue::String(s) if s == "*")));
192            }
193            other => panic!("expected In, got {other:?}"),
194        }
195    }
196
197    #[test]
198    fn master_applies_no_constraint_at_all() {
199        assert!(AclScope::Unrestricted.read_constraint().is_none());
200        assert!(AclScope::Unrestricted.write_constraint().is_none());
201    }
202
203    #[test]
204    fn a_user_scope_carries_its_object_id() {
205        let c = AclScope::User {
206            object_id: "u1".into(),
207        }
208        .read_constraint()
209        .expect("constrained");
210        match c.comparison {
211            Comparison::In(values) => assert!(values
212                .iter()
213                .any(|v| matches!(v, ParseValue::String(s) if s == "u1"))),
214            other => panic!("expected In, got {other:?}"),
215        }
216    }
217
218    #[test]
219    fn acl_lowers_to_two_columns_and_raises_back() {
220        let mut acl_map = ParseMap::new();
221        let mut public = ParseMap::new();
222        public.insert("read".into(), ParseValue::Bool(true));
223        acl_map.insert("*".into(), ParseValue::Object(public));
224        let mut owner = ParseMap::new();
225        owner.insert("read".into(), ParseValue::Bool(true));
226        owner.insert("write".into(), ParseValue::Bool(true));
227        acl_map.insert("u1".into(), ParseValue::Object(owner));
228
229        let lowered = lower_acl(row(vec![
230            ("title", ParseValue::String("x".into())),
231            ("ACL", ParseValue::Object(acl_map)),
232        ]));
233        assert!(
234            lowered.get("ACL").is_none(),
235            "ACL must not be stored as a field"
236        );
237        assert!(matches!(lowered.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 2));
238        assert!(matches!(lowered.get("_wperm"), Some(ParseValue::Array(a)) if a.len() == 1));
239
240        let raised = raise_acl(lowered);
241        assert!(raised.get("_rperm").is_none() && raised.get("_wperm").is_none());
242        let ParseValue::Object(acl) = raised.get("ACL").expect("ACL restored") else {
243            panic!("ACL should be an object");
244        };
245        assert!(acl.contains_key("*") && acl.contains_key("u1"));
246    }
247
248    /// UPSTREAM-QUIRK, reproduced end to end.
249    #[test]
250    fn a_false_flag_disappears_on_the_round_trip() {
251        let mut entry = ParseMap::new();
252        entry.insert("read".into(), ParseValue::Bool(true));
253        entry.insert("write".into(), ParseValue::Bool(false));
254        let mut acl_map = ParseMap::new();
255        acl_map.insert("*".into(), ParseValue::Object(entry));
256
257        let raised = raise_acl(lower_acl(row(vec![("ACL", ParseValue::Object(acl_map))])));
258        let ParseValue::Object(acl) = raised.get("ACL").expect("ACL") else {
259            panic!()
260        };
261        let ParseValue::Object(star) = acl.get("*").expect("*") else {
262            panic!()
263        };
264        assert!(star.contains_key("read"));
265        assert!(
266            !star.contains_key("write"),
267            "the false key is dropped, matching untransformObjectACL"
268        );
269    }
270
271    #[test]
272    fn a_row_with_no_acl_gets_no_columns_and_no_acl_key_back() {
273        let lowered = lower_acl(row(vec![("title", ParseValue::String("x".into()))]));
274        assert!(lowered.get("_rperm").is_none());
275        let raised = raise_acl(lowered);
276        assert!(
277            raised.get("ACL").is_none(),
278            "absent columns produce no ACL key at all, not null and not an empty object"
279        );
280    }
281}