1use parse_rust_core::{Acl, ErrorCode, ParseError, 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 {
30 object_id: String,
31 roles: Vec<String>,
32 },
33}
34
35impl AclScope {
36 pub fn user(object_id: impl Into<String>, roles: Vec<String>) -> Result<Self, ParseError> {
43 let object_id = object_id.into();
44 if object_id.starts_with("role:") {
45 return Err(ParseError::new(
46 ErrorCode::InternalServerError,
47 "Invalid object ID.",
48 ));
49 }
50 Ok(AclScope::User { object_id, roles })
51 }
52
53 pub fn is_master(&self) -> bool {
54 matches!(self, AclScope::Unrestricted)
55 }
56
57 pub fn user_id(&self) -> Option<&str> {
58 match self {
59 AclScope::User { object_id, .. } => Some(object_id),
60 _ => None,
61 }
62 }
63
64 pub fn has_role(&self, name: &str) -> bool {
66 match self {
67 AclScope::User { roles, .. } => roles.iter().any(|r| r == name),
68 _ => false,
69 }
70 }
71
72 pub fn acl_group(&self) -> Vec<String> {
82 match self {
83 AclScope::Unrestricted => Vec::new(),
84 AclScope::Anonymous => vec!["*".to_string()],
85 AclScope::User { object_id, roles } => {
86 let mut out = Vec::with_capacity(roles.len() + 2);
87 out.push("*".to_string());
88 out.extend(roles.iter().map(|r| format!("role:{r}")));
89 if !object_id.starts_with("role:") {
93 out.push(object_id.clone());
94 }
95 out
96 }
97 }
98 }
99
100 fn principals(&self, seed_public: bool) -> Vec<ParseValue> {
105 let mut out = vec![ParseValue::Null];
107 if seed_public {
108 out.push(ParseValue::String("*".to_string()));
109 }
110 out.extend(self.acl_group().into_iter().map(ParseValue::String));
111 out
112 }
113
114 pub fn read_constraint(&self) -> Option<Constraint> {
125 match self {
126 AclScope::Unrestricted => None,
127 _ => Some(Constraint {
128 field: "_rperm".to_string(),
129 comparison: Comparison::In(self.principals(true)),
130 }),
131 }
132 }
133
134 pub fn write_constraint(&self) -> Option<Constraint> {
141 match self {
142 AclScope::Unrestricted => None,
143 _ => Some(Constraint {
144 field: "_wperm".to_string(),
145 comparison: Comparison::In(self.principals(false)),
146 }),
147 }
148 }
149}
150
151pub fn lower_acl(mut row: ParseMap) -> ParseMap {
174 let Some(acl_value) = row.shift_remove("ACL") else {
175 return row;
176 };
177 if !parse_rust_core::is_js_truthy(&acl_value) {
178 return row;
179 }
180 let acl = acl_from_value(&acl_value).unwrap_or_default();
182 let (rperm, wperm) = acl.to_perms();
183 row.insert(
184 "_rperm".to_string(),
185 ParseValue::Array(rperm.into_iter().map(ParseValue::String).collect()),
186 );
187 row.insert(
188 "_wperm".to_string(),
189 ParseValue::Array(wperm.into_iter().map(ParseValue::String).collect()),
190 );
191 row
192}
193
194pub fn raise_acl(mut row: ParseMap) -> ParseMap {
199 let rperm = take_string_array(&mut row, "_rperm");
200 let wperm = take_string_array(&mut row, "_wperm");
201
202 let Some(acl) = Acl::from_perms(rperm.as_deref(), wperm.as_deref()) else {
203 return row;
204 };
205
206 let mut map = ParseMap::new();
207 for (principal, perms) in acl.iter() {
208 if perms.is_empty() {
209 continue;
210 }
211 let mut entry = ParseMap::new();
212 if perms.read {
214 entry.insert("read".to_string(), ParseValue::Bool(true));
215 }
216 if perms.write {
217 entry.insert("write".to_string(), ParseValue::Bool(true));
218 }
219 map.insert(principal.as_key(), ParseValue::Object(entry));
220 }
221 row.insert("ACL".to_string(), ParseValue::Object(map));
222 row
223}
224
225fn take_string_array(row: &mut ParseMap, key: &str) -> Option<Vec<String>> {
226 match row.shift_remove(key) {
227 Some(ParseValue::Array(items)) => Some(
228 items
229 .into_iter()
230 .filter_map(|v| match v {
231 ParseValue::String(s) => Some(s),
232 _ => None,
233 })
234 .collect(),
235 ),
236 _ => None,
237 }
238}
239
240fn acl_from_value(value: &ParseValue) -> Option<Acl> {
242 let ParseValue::Object(map) = value else {
243 return None;
244 };
245 let mut acl = Acl::new();
246 for (key, entry) in map {
247 let ParseValue::Object(flags) = entry else {
248 continue;
249 };
250 let flag = |name: &str| matches!(flags.get(name), Some(ParseValue::Bool(true)));
251 acl.set(
252 Principal::parse(key),
253 Permissions {
254 read: flag("read"),
255 write: flag("write"),
256 },
257 );
258 }
259 Some(acl)
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
267 let mut m = ParseMap::new();
268 for (k, v) in pairs {
269 m.insert(k.to_string(), v);
270 }
271 m
272 }
273
274 #[test]
276 fn the_read_constraint_includes_null_so_public_rows_stay_visible() {
277 let c = AclScope::Anonymous
278 .read_constraint()
279 .expect("anonymous is constrained");
280 assert_eq!(c.field, "_rperm");
281 match c.comparison {
282 Comparison::In(values) => {
283 assert!(
284 values.iter().any(|v| matches!(v, ParseValue::Null)),
285 "null must be in the list, or every row saved without an ACL becomes invisible"
286 );
287 assert!(values
288 .iter()
289 .any(|v| matches!(v, ParseValue::String(s) if s == "*")));
290 }
291 other => panic!("expected In, got {other:?}"),
292 }
293 }
294
295 #[test]
301 fn a_truthy_non_object_acl_writes_empty_columns_rather_than_none() {
302 for value in [
303 ParseValue::String("x".into()),
304 ParseValue::Number(1.0),
305 ParseValue::Array(vec![]),
306 ParseValue::Array(vec![ParseValue::String("*".into())]),
307 ParseValue::Bool(true),
308 ParseValue::Object(ParseMap::new()),
309 ] {
310 let lowered = lower_acl(row(vec![("ACL", value.clone())]));
311 for column in ["_rperm", "_wperm"] {
312 assert!(
313 matches!(lowered.get(column), Some(ParseValue::Array(a)) if a.is_empty()),
314 "a truthy ACL must write an empty {column}, got {:?} for {value:?}",
315 lowered.get(column)
316 );
317 }
318 assert!(!lowered.contains_key("ACL"));
319 }
320 }
321
322 #[test]
325 fn a_falsy_or_absent_acl_writes_no_columns() {
326 for value in [
327 ParseValue::Null,
328 ParseValue::Bool(false),
329 ParseValue::Number(0.0),
330 ParseValue::String(String::new()),
331 ] {
332 let lowered = lower_acl(row(vec![("ACL", value.clone())]));
333 assert!(!lowered.contains_key("_rperm"), "falsy ACL: {value:?}");
334 assert!(!lowered.contains_key("_wperm"), "falsy ACL: {value:?}");
335 }
336 let untouched = lower_acl(row(vec![("title", ParseValue::String("x".into()))]));
337 assert!(!untouched.contains_key("_rperm"));
338 assert!(!untouched.contains_key("_wperm"));
339 }
340
341 #[test]
342 fn master_applies_no_constraint_at_all() {
343 assert!(AclScope::Unrestricted.read_constraint().is_none());
344 assert!(AclScope::Unrestricted.write_constraint().is_none());
345 }
346
347 #[test]
348 fn a_user_scope_carries_its_object_id() {
349 let c = AclScope::user("u1", vec![])
350 .expect("plain id")
351 .read_constraint()
352 .expect("constrained");
353 match c.comparison {
354 Comparison::In(values) => assert!(values
355 .iter()
356 .any(|v| matches!(v, ParseValue::String(s) if s == "u1"))),
357 other => panic!("expected In, got {other:?}"),
358 }
359 }
360
361 #[test]
364 fn a_role_entry_now_matches() {
365 let scope = AclScope::user("u1", vec!["Admins".into(), "Editors".into()]).expect("scope");
366 let c = scope.read_constraint().expect("constrained");
367 match c.comparison {
368 Comparison::In(values) => {
369 let strings: Vec<&str> = values
370 .iter()
371 .filter_map(|v| match v {
372 ParseValue::String(s) => Some(s.as_str()),
373 _ => None,
374 })
375 .collect();
376 assert!(strings.contains(&"role:Admins"), "{strings:?}");
377 assert!(strings.contains(&"role:Editors"), "{strings:?}");
378 assert!(strings.contains(&"u1"));
379 }
380 other => panic!("expected In, got {other:?}"),
381 }
382 }
383
384 #[test]
387 fn the_acl_group_is_star_then_roles_then_the_user() {
388 let scope = AclScope::user("u1", vec!["A".into()]).expect("scope");
389 assert_eq!(scope.acl_group(), vec!["*", "role:A", "u1"]);
390 assert_eq!(AclScope::Anonymous.acl_group(), vec!["*"]);
391 assert!(AclScope::Unrestricted.acl_group().is_empty());
392 }
393
394 #[test]
395 fn accessors_answer_for_every_variant() {
396 let scope = AclScope::user("u1", vec!["A".into()]).expect("scope");
397 assert!(scope.has_role("A"));
398 assert!(!scope.has_role("role:A"), "roles are stored bare");
399 assert!(!scope.has_role("B"));
400 assert_eq!(scope.user_id(), Some("u1"));
401 assert!(!scope.is_master());
402 assert!(AclScope::Unrestricted.is_master());
403 assert_eq!(AclScope::Anonymous.user_id(), None);
404 assert!(!AclScope::Anonymous.has_role("A"));
405 }
406
407 #[test]
409 fn a_role_prefixed_object_id_is_refused() {
410 let e = AclScope::user("role:Admins", vec![]).unwrap_err();
411 assert_eq!(e.code, parse_rust_core::ErrorCode::InternalServerError);
412 assert_eq!(e.message, "Invalid object ID.");
413 }
414
415 #[test]
416 fn acl_lowers_to_two_columns_and_raises_back() {
417 let mut acl_map = ParseMap::new();
418 let mut public = ParseMap::new();
419 public.insert("read".into(), ParseValue::Bool(true));
420 acl_map.insert("*".into(), ParseValue::Object(public));
421 let mut owner = ParseMap::new();
422 owner.insert("read".into(), ParseValue::Bool(true));
423 owner.insert("write".into(), ParseValue::Bool(true));
424 acl_map.insert("u1".into(), ParseValue::Object(owner));
425
426 let lowered = lower_acl(row(vec![
427 ("title", ParseValue::String("x".into())),
428 ("ACL", ParseValue::Object(acl_map)),
429 ]));
430 assert!(
431 lowered.get("ACL").is_none(),
432 "ACL must not be stored as a field"
433 );
434 assert!(matches!(lowered.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 2));
435 assert!(matches!(lowered.get("_wperm"), Some(ParseValue::Array(a)) if a.len() == 1));
436
437 let raised = raise_acl(lowered);
438 assert!(raised.get("_rperm").is_none() && raised.get("_wperm").is_none());
439 let ParseValue::Object(acl) = raised.get("ACL").expect("ACL restored") else {
440 panic!("ACL should be an object");
441 };
442 assert!(acl.contains_key("*") && acl.contains_key("u1"));
443 }
444
445 #[test]
447 fn a_false_flag_disappears_on_the_round_trip() {
448 let mut entry = ParseMap::new();
449 entry.insert("read".into(), ParseValue::Bool(true));
450 entry.insert("write".into(), ParseValue::Bool(false));
451 let mut acl_map = ParseMap::new();
452 acl_map.insert("*".into(), ParseValue::Object(entry));
453
454 let raised = raise_acl(lower_acl(row(vec![("ACL", ParseValue::Object(acl_map))])));
455 let ParseValue::Object(acl) = raised.get("ACL").expect("ACL") else {
456 panic!()
457 };
458 let ParseValue::Object(star) = acl.get("*").expect("*") else {
459 panic!()
460 };
461 assert!(star.contains_key("read"));
462 assert!(
463 !star.contains_key("write"),
464 "the false key is dropped, matching untransformObjectACL"
465 );
466 }
467
468 #[test]
469 fn a_row_with_no_acl_gets_no_columns_and_no_acl_key_back() {
470 let lowered = lower_acl(row(vec![("title", ParseValue::String("x".into()))]));
471 assert!(lowered.get("_rperm").is_none());
472 let raised = raise_acl(lowered);
473 assert!(
474 raised.get("ACL").is_none(),
475 "absent columns produce no ACL key at all, not null and not an empty object"
476 );
477 }
478}