Skip to main content

parse_rust_core/
acl.rs

1//! Parse ACLs, and the storage form they lower to.
2//!
3//! An ACL is a map from principal to permissions. On the wire it is
4//! `{"*":{"read":true},"role:Admin":{"read":true,"write":true}}`. In storage it is two arrays,
5//! `_rperm` and `_wperm`, each listing the principals holding that permission.
6//!
7//! **UPSTREAM-QUIRK: the round trip is lossy, and this is Tier 1 bug-compatible.**
8//! `untransformObjectACL` (`DatabaseController.js:385-406`) rebuilds the ACL from the two arrays
9//! and only ever *sets* `read: true` or `write: true`. It never writes `false`. So an entry saved
10//! as `{"read":true,"write":false}` reads back as `{"read":true}`, with the false key gone. A
11//! client could depend on either shape, so reproduce it exactly and do not "fix" it.
12//!
13//! Two adjacent behaviors from the same function, both reproduced here:
14//! - **Absent `_rperm` and `_wperm` produce no `ACL` key at all**, not `null` and not `{}`.
15//!   The guard is `if (_rperm || _wperm)`.
16//! - **An empty array is truthy in JavaScript**, so `_rperm: []` with no `_wperm` produces
17//!   `"ACL": {}`, an empty object rather than an absent key. That asymmetry is easy to miss and
18//!   easy to get wrong in Rust, where both are naturally `Option`/empty-`Vec`.
19
20use indexmap::IndexMap;
21
22/// Who a permission applies to.
23///
24/// `Other` exists because upstream does not validate that a non-`*`, non-`role:` principal is a
25/// well-formed objectId. `RestWrite.js` writes the sentinel `*unresolved` into `_rperm` during
26/// pointer-permission resolution and later matches on it, so a type that assumed "everything
27/// else is an ObjectId" would either reject a real stored value or silently normalize it.
28#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub enum Principal {
30    /// `"*"`
31    Public,
32    /// `"role:<name>"`, stored without the prefix.
33    Role(String),
34    /// Anything else, kept verbatim. Usually an objectId.
35    Other(String),
36}
37
38impl Principal {
39    pub fn parse(s: &str) -> Self {
40        if s == "*" {
41            Principal::Public
42        } else if let Some(name) = s.strip_prefix("role:") {
43            Principal::Role(name.to_string())
44        } else {
45            Principal::Other(s.to_string())
46        }
47    }
48
49    pub fn as_key(&self) -> String {
50        match self {
51            Principal::Public => "*".to_string(),
52            Principal::Role(name) => format!("role:{name}"),
53            Principal::Other(s) => s.clone(),
54        }
55    }
56}
57
58/// Read and write flags for one principal.
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub struct Permissions {
61    pub read: bool,
62    pub write: bool,
63}
64
65impl Permissions {
66    pub fn is_empty(&self) -> bool {
67        !self.read && !self.write
68    }
69}
70
71/// An ACL. Insertion-ordered, because the order of `_rperm` and `_wperm` is observable in
72/// golden-file comparison even though it carries no meaning.
73#[derive(Clone, Debug, Default, PartialEq, Eq)]
74pub struct Acl(IndexMap<Principal, Permissions>);
75
76impl Acl {
77    pub fn new() -> Self {
78        Self(IndexMap::new())
79    }
80
81    pub fn set(&mut self, principal: Principal, perms: Permissions) {
82        self.0.insert(principal, perms);
83    }
84
85    pub fn get(&self, principal: &Principal) -> Option<Permissions> {
86        self.0.get(principal).copied()
87    }
88
89    pub fn is_empty(&self) -> bool {
90        self.0.is_empty()
91    }
92
93    pub fn iter(&self) -> impl Iterator<Item = (&Principal, &Permissions)> {
94        self.0.iter()
95    }
96
97    /// Lower to the storage form. An entry with neither permission appears in neither array,
98    /// which is what makes the JSON round trip lossy.
99    pub fn to_perms(&self) -> (Vec<String>, Vec<String>) {
100        let mut rperm = Vec::new();
101        let mut wperm = Vec::new();
102        for (principal, perms) in &self.0 {
103            if perms.read {
104                rperm.push(principal.as_key());
105            }
106            if perms.write {
107                wperm.push(principal.as_key());
108            }
109        }
110        (rperm, wperm)
111    }
112
113    /// Rebuild from the storage form, reproducing `untransformObjectACL` exactly.
114    ///
115    /// Returns `None` when both columns are absent, which is the case that must produce no `ACL`
116    /// key in the response rather than an empty one. `Some(empty Acl)` is the distinct case where
117    /// a column is present but empty, which upstream renders as `"ACL": {}`.
118    pub fn from_perms(rperm: Option<&[String]>, wperm: Option<&[String]>) -> Option<Acl> {
119        if rperm.is_none() && wperm.is_none() {
120            return None;
121        }
122        let mut acl = Acl::new();
123        for entry in rperm.unwrap_or(&[]) {
124            acl.0.entry(Principal::parse(entry)).or_default().read = true;
125        }
126        for entry in wperm.unwrap_or(&[]) {
127            acl.0.entry(Principal::parse(entry)).or_default().write = true;
128        }
129        Some(acl)
130    }
131
132    /// The wire form. Only true flags are emitted, per the quirk above.
133    pub fn to_json(&self) -> String {
134        let mut out = String::from("{");
135        let mut first = true;
136        for (principal, perms) in &self.0 {
137            if perms.is_empty() {
138                continue;
139            }
140            if !first {
141                out.push(',');
142            }
143            first = false;
144            crate::value::write_json_string(&principal.as_key(), &mut out);
145            out.push(':');
146            out.push('{');
147            match (perms.read, perms.write) {
148                (true, true) => out.push_str(r#""read":true,"write":true"#),
149                (true, false) => out.push_str(r#""read":true"#),
150                (false, true) => out.push_str(r#""write":true"#),
151                (false, false) => {}
152            }
153            out.push('}');
154        }
155        out.push('}');
156        out
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    fn p(s: &str) -> Principal {
165        Principal::parse(s)
166    }
167
168    #[test]
169    fn principal_round_trips_every_shape() {
170        for s in [
171            "*",
172            "role:Admin",
173            "abc123",
174            "*unresolved",
175            "role:with:colons",
176        ] {
177            assert_eq!(Principal::parse(s).as_key(), s, "{s} did not round trip");
178        }
179        assert_eq!(p("*"), Principal::Public);
180        assert_eq!(p("role:Admin"), Principal::Role("Admin".into()));
181        // The sentinel must not be mistaken for the public principal or an objectId.
182        assert_eq!(p("*unresolved"), Principal::Other("*unresolved".into()));
183    }
184
185    #[test]
186    fn storage_round_trip_is_lossless() {
187        let mut acl = Acl::new();
188        acl.set(
189            Principal::Public,
190            Permissions {
191                read: true,
192                write: false,
193            },
194        );
195        acl.set(
196            Principal::Role("Admin".into()),
197            Permissions {
198                read: true,
199                write: true,
200            },
201        );
202
203        let (r, w) = acl.to_perms();
204        assert_eq!(r, vec!["*", "role:Admin"]);
205        assert_eq!(w, vec!["role:Admin"]);
206        assert_eq!(Acl::from_perms(Some(&r), Some(&w)), Some(acl));
207    }
208
209    /// The Tier 1 quirk. This test exists so that "fixing" it fails loudly.
210    #[test]
211    fn upstream_quirk_false_flags_are_dropped_from_json() {
212        let mut acl = Acl::new();
213        acl.set(
214            Principal::Public,
215            Permissions {
216                read: true,
217                write: false,
218            },
219        );
220        assert_eq!(acl.to_json(), r#"{"*":{"read":true}}"#);
221
222        let mut both = Acl::new();
223        both.set(
224            Principal::Public,
225            Permissions {
226                read: true,
227                write: true,
228            },
229        );
230        assert_eq!(both.to_json(), r#"{"*":{"read":true,"write":true}}"#);
231
232        let mut write_only = Acl::new();
233        write_only.set(
234            Principal::Public,
235            Permissions {
236                read: false,
237                write: true,
238            },
239        );
240        assert_eq!(write_only.to_json(), r#"{"*":{"write":true}}"#);
241    }
242
243    #[test]
244    fn an_entry_with_no_permissions_disappears_entirely() {
245        let mut acl = Acl::new();
246        acl.set(Principal::Other("abc".into()), Permissions::default());
247        assert_eq!(acl.to_json(), "{}");
248        let (r, w) = acl.to_perms();
249        assert!(r.is_empty() && w.is_empty());
250    }
251
252    #[test]
253    fn absent_columns_and_empty_columns_are_different() {
254        // Both absent: no ACL key at all in the response.
255        assert_eq!(Acl::from_perms(None, None), None);
256        // Present but empty: an empty ACL object, because [] is truthy in JavaScript.
257        let empty: [String; 0] = [];
258        assert_eq!(Acl::from_perms(Some(&empty), None), Some(Acl::new()));
259        assert_eq!(
260            Acl::from_perms(Some(&empty), None).map(|a| a.to_json()),
261            Some("{}".to_string())
262        );
263    }
264
265    #[test]
266    fn merges_the_two_columns_per_principal() {
267        let r = vec!["*".to_string(), "role:Admin".to_string()];
268        let w = vec!["role:Admin".to_string()];
269        let acl = Acl::from_perms(Some(&r), Some(&w)).unwrap();
270        assert_eq!(
271            acl.get(&Principal::Public),
272            Some(Permissions {
273                read: true,
274                write: false
275            })
276        );
277        assert_eq!(
278            acl.get(&Principal::Role("Admin".into())),
279            Some(Permissions {
280                read: true,
281                write: true
282            })
283        );
284    }
285
286    #[test]
287    fn insertion_order_is_preserved_in_both_directions() {
288        let r = vec!["z".to_string(), "a".to_string(), "m".to_string()];
289        let acl = Acl::from_perms(Some(&r), None).unwrap();
290        let (out, _) = acl.to_perms();
291        assert_eq!(out, r, "order must not be sorted");
292    }
293}