1use parse_rust_core::{Acl, ParseMap, ParseValue, Permissions, Principal};
10use parse_rust_storage::{Comparison, Constraint};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum AclScope {
19 Unrestricted,
21 Anonymous,
23 User { object_id: String },
25}
26
27impl AclScope {
28 fn principals(&self) -> Vec<ParseValue> {
30 let mut out = vec![
31 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 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 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
73pub 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
96pub 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 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
142fn 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 #[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 #[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}