use parse_rust_core::{Acl, ErrorCode, ParseError, ParseMap, ParseValue, Permissions, Principal};
use parse_rust_storage::{Comparison, Constraint};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AclScope {
Unrestricted,
Anonymous,
User {
object_id: String,
roles: Vec<String>,
},
}
impl AclScope {
pub fn user(object_id: impl Into<String>, roles: Vec<String>) -> Result<Self, ParseError> {
let object_id = object_id.into();
if object_id.starts_with("role:") {
return Err(ParseError::new(
ErrorCode::InternalServerError,
"Invalid object ID.",
));
}
Ok(AclScope::User { object_id, roles })
}
pub fn is_master(&self) -> bool {
matches!(self, AclScope::Unrestricted)
}
pub fn user_id(&self) -> Option<&str> {
match self {
AclScope::User { object_id, .. } => Some(object_id),
_ => None,
}
}
pub fn has_role(&self, name: &str) -> bool {
match self {
AclScope::User { roles, .. } => roles.iter().any(|r| r == name),
_ => false,
}
}
pub fn acl_group(&self) -> Vec<String> {
match self {
AclScope::Unrestricted => Vec::new(),
AclScope::Anonymous => vec!["*".to_string()],
AclScope::User { object_id, roles } => {
let mut out = Vec::with_capacity(roles.len() + 2);
out.push("*".to_string());
out.extend(roles.iter().map(|r| format!("role:{r}")));
if !object_id.starts_with("role:") {
out.push(object_id.clone());
}
out
}
}
}
fn principals(&self, seed_public: bool) -> Vec<ParseValue> {
let mut out = vec![ParseValue::Null];
if seed_public {
out.push(ParseValue::String("*".to_string()));
}
out.extend(self.acl_group().into_iter().map(ParseValue::String));
out
}
pub fn read_constraint(&self) -> Option<Constraint> {
match self {
AclScope::Unrestricted => None,
_ => Some(Constraint {
field: "_rperm".to_string(),
comparison: Comparison::In(self.principals(true)),
}),
}
}
pub fn write_constraint(&self) -> Option<Constraint> {
match self {
AclScope::Unrestricted => None,
_ => Some(Constraint {
field: "_wperm".to_string(),
comparison: Comparison::In(self.principals(false)),
}),
}
}
}
pub fn default_acl_for_create(declared: &ParseValue, caller: Option<&str>) -> ParseValue {
let ParseValue::Object(map) = declared else {
return declared.clone();
};
let mut acl = map.clone();
let Some(current_user) = acl.get("currentUser").cloned() else {
return ParseValue::Object(acl);
};
if !parse_rust_core::is_js_truthy(¤t_user) {
return ParseValue::Object(acl);
}
if let Some(caller) = caller {
acl.insert(caller.to_string(), current_user);
}
acl.shift_remove("currentUser");
ParseValue::Object(acl)
}
pub fn lower_acl(mut row: ParseMap) -> ParseMap {
let Some(acl_value) = row.shift_remove("ACL") else {
return row;
};
if !parse_rust_core::is_js_truthy(&acl_value) {
return row;
}
let acl = acl_from_value(&acl_value).unwrap_or_default();
let (rperm, wperm) = acl.to_perms();
row.insert(
"_rperm".to_string(),
ParseValue::Array(rperm.into_iter().map(ParseValue::String).collect()),
);
row.insert(
"_wperm".to_string(),
ParseValue::Array(wperm.into_iter().map(ParseValue::String).collect()),
);
row
}
pub fn raise_acl(mut row: ParseMap) -> ParseMap {
let rperm = take_string_array(&mut row, "_rperm");
let wperm = take_string_array(&mut row, "_wperm");
let Some(acl) = Acl::from_perms(rperm.as_deref(), wperm.as_deref()) else {
return row;
};
let mut map = ParseMap::new();
for (principal, perms) in acl.iter() {
if perms.is_empty() {
continue;
}
let mut entry = ParseMap::new();
if perms.read {
entry.insert("read".to_string(), ParseValue::Bool(true));
}
if perms.write {
entry.insert("write".to_string(), ParseValue::Bool(true));
}
map.insert(principal.as_key(), ParseValue::Object(entry));
}
row.insert("ACL".to_string(), ParseValue::Object(map));
row
}
fn take_string_array(row: &mut ParseMap, key: &str) -> Option<Vec<String>> {
match row.shift_remove(key) {
Some(ParseValue::Array(items)) => Some(
items
.into_iter()
.filter_map(|v| match v {
ParseValue::String(s) => Some(s),
_ => None,
})
.collect(),
),
_ => None,
}
}
fn acl_from_value(value: &ParseValue) -> Option<Acl> {
let ParseValue::Object(map) = value else {
return None;
};
let mut acl = Acl::new();
for (key, entry) in map {
let ParseValue::Object(flags) = entry else {
continue;
};
let flag = |name: &str| matches!(flags.get(name), Some(ParseValue::Bool(true)));
acl.set(
Principal::parse(key),
Permissions {
read: flag("read"),
write: flag("write"),
},
);
}
Some(acl)
}
#[cfg(test)]
mod tests {
use super::*;
fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
let mut m = ParseMap::new();
for (k, v) in pairs {
m.insert(k.to_string(), v);
}
m
}
#[test]
fn the_read_constraint_includes_null_so_public_rows_stay_visible() {
let c = AclScope::Anonymous
.read_constraint()
.expect("anonymous is constrained");
assert_eq!(c.field, "_rperm");
match c.comparison {
Comparison::In(values) => {
assert!(
values.iter().any(|v| matches!(v, ParseValue::Null)),
"null must be in the list, or every row saved without an ACL becomes invisible"
);
assert!(values
.iter()
.any(|v| matches!(v, ParseValue::String(s) if s == "*")));
}
other => panic!("expected In, got {other:?}"),
}
}
#[test]
fn a_permission_bearing_array_currently_grants_nobody() {
let mut entry = ParseMap::new();
entry.insert("read".into(), ParseValue::Bool(true));
let lowered = lower_acl(row(vec![(
"ACL",
ParseValue::Array(vec![ParseValue::Object(entry)]),
)]));
for column in ["_rperm", "_wperm"] {
assert!(
matches!(lowered.get(column), Some(ParseValue::Array(a)) if a.is_empty()),
"upstream grants principal \"0\" here; parse-rust grants nobody, and the row \
records it. Got {:?} for {column}",
lowered.get(column)
);
}
}
#[test]
fn a_truthy_non_object_acl_writes_empty_columns_rather_than_none() {
for value in [
ParseValue::String("x".into()),
ParseValue::Number(1.0),
ParseValue::Array(vec![]),
ParseValue::Array(vec![ParseValue::String("*".into())]),
ParseValue::Bool(true),
ParseValue::Object(ParseMap::new()),
] {
let lowered = lower_acl(row(vec![("ACL", value.clone())]));
for column in ["_rperm", "_wperm"] {
assert!(
matches!(lowered.get(column), Some(ParseValue::Array(a)) if a.is_empty()),
"a truthy ACL must write an empty {column}, got {:?} for {value:?}",
lowered.get(column)
);
}
assert!(!lowered.contains_key("ACL"));
}
}
#[test]
fn a_falsy_or_absent_acl_writes_no_columns() {
for value in [
ParseValue::Null,
ParseValue::Bool(false),
ParseValue::Number(0.0),
ParseValue::String(String::new()),
] {
let lowered = lower_acl(row(vec![("ACL", value.clone())]));
assert!(!lowered.contains_key("_rperm"), "falsy ACL: {value:?}");
assert!(!lowered.contains_key("_wperm"), "falsy ACL: {value:?}");
}
let untouched = lower_acl(row(vec![("title", ParseValue::String("x".into()))]));
assert!(!untouched.contains_key("_rperm"));
assert!(!untouched.contains_key("_wperm"));
}
#[test]
fn master_applies_no_constraint_at_all() {
assert!(AclScope::Unrestricted.read_constraint().is_none());
assert!(AclScope::Unrestricted.write_constraint().is_none());
}
#[test]
fn a_user_scope_carries_its_object_id() {
let c = AclScope::user("u1", vec![])
.expect("plain id")
.read_constraint()
.expect("constrained");
match c.comparison {
Comparison::In(values) => assert!(values
.iter()
.any(|v| matches!(v, ParseValue::String(s) if s == "u1"))),
other => panic!("expected In, got {other:?}"),
}
}
#[test]
fn a_role_entry_now_matches() {
let scope = AclScope::user("u1", vec!["Admins".into(), "Editors".into()]).expect("scope");
let c = scope.read_constraint().expect("constrained");
match c.comparison {
Comparison::In(values) => {
let strings: Vec<&str> = values
.iter()
.filter_map(|v| match v {
ParseValue::String(s) => Some(s.as_str()),
_ => None,
})
.collect();
assert!(strings.contains(&"role:Admins"), "{strings:?}");
assert!(strings.contains(&"role:Editors"), "{strings:?}");
assert!(strings.contains(&"u1"));
}
other => panic!("expected In, got {other:?}"),
}
}
#[test]
fn the_acl_group_is_star_then_roles_then_the_user() {
let scope = AclScope::user("u1", vec!["A".into()]).expect("scope");
assert_eq!(scope.acl_group(), vec!["*", "role:A", "u1"]);
assert_eq!(AclScope::Anonymous.acl_group(), vec!["*"]);
assert!(AclScope::Unrestricted.acl_group().is_empty());
}
#[test]
fn accessors_answer_for_every_variant() {
let scope = AclScope::user("u1", vec!["A".into()]).expect("scope");
assert!(scope.has_role("A"));
assert!(!scope.has_role("role:A"), "roles are stored bare");
assert!(!scope.has_role("B"));
assert_eq!(scope.user_id(), Some("u1"));
assert!(!scope.is_master());
assert!(AclScope::Unrestricted.is_master());
assert_eq!(AclScope::Anonymous.user_id(), None);
assert!(!AclScope::Anonymous.has_role("A"));
}
#[test]
fn a_role_prefixed_object_id_is_refused() {
let e = AclScope::user("role:Admins", vec![]).unwrap_err();
assert_eq!(e.code, parse_rust_core::ErrorCode::InternalServerError);
assert_eq!(e.message, "Invalid object ID.");
}
#[test]
fn acl_lowers_to_two_columns_and_raises_back() {
let mut acl_map = ParseMap::new();
let mut public = ParseMap::new();
public.insert("read".into(), ParseValue::Bool(true));
acl_map.insert("*".into(), ParseValue::Object(public));
let mut owner = ParseMap::new();
owner.insert("read".into(), ParseValue::Bool(true));
owner.insert("write".into(), ParseValue::Bool(true));
acl_map.insert("u1".into(), ParseValue::Object(owner));
let lowered = lower_acl(row(vec![
("title", ParseValue::String("x".into())),
("ACL", ParseValue::Object(acl_map)),
]));
assert!(
lowered.get("ACL").is_none(),
"ACL must not be stored as a field"
);
assert!(matches!(lowered.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 2));
assert!(matches!(lowered.get("_wperm"), Some(ParseValue::Array(a)) if a.len() == 1));
let raised = raise_acl(lowered);
assert!(raised.get("_rperm").is_none() && raised.get("_wperm").is_none());
let ParseValue::Object(acl) = raised.get("ACL").expect("ACL restored") else {
panic!("ACL should be an object");
};
assert!(acl.contains_key("*") && acl.contains_key("u1"));
}
#[test]
fn a_false_flag_disappears_on_the_round_trip() {
let mut entry = ParseMap::new();
entry.insert("read".into(), ParseValue::Bool(true));
entry.insert("write".into(), ParseValue::Bool(false));
let mut acl_map = ParseMap::new();
acl_map.insert("*".into(), ParseValue::Object(entry));
let raised = raise_acl(lower_acl(row(vec![("ACL", ParseValue::Object(acl_map))])));
let ParseValue::Object(acl) = raised.get("ACL").expect("ACL") else {
panic!()
};
let ParseValue::Object(star) = acl.get("*").expect("*") else {
panic!()
};
assert!(star.contains_key("read"));
assert!(
!star.contains_key("write"),
"the false key is dropped, matching untransformObjectACL"
);
}
fn declared(json: &str) -> ParseValue {
parse_rust_core::decode::classify(
serde_json::from_str(json).expect("test literal must be valid JSON"),
)
.expect("classify")
}
fn principals(acl: ParseValue) -> (Vec<String>, Vec<String>) {
let mut carrier = ParseMap::new();
carrier.insert("ACL".to_string(), acl);
let lowered = lower_acl(carrier);
let read = take_string_array(&mut lowered.clone(), "_rperm").unwrap_or_default();
let write = take_string_array(&mut lowered.clone(), "_wperm").unwrap_or_default();
(read, write)
}
#[test]
fn current_user_resolves_to_the_callers_object_id() {
let acl = default_acl_for_create(
&declared(r#"{"currentUser":{"read":true,"write":true}}"#),
Some("userA"),
);
let ParseValue::Object(map) = &acl else {
panic!("expected an object")
};
assert!(
!map.contains_key("currentUser"),
"the literal key matches nobody and must not be stored"
);
assert_eq!(
principals(acl),
(vec!["userA".to_string()], vec!["userA".to_string()])
);
}
#[test]
fn a_read_only_declaration_produces_a_read_only_row() {
let acl = default_acl_for_create(&declared(r#"{"currentUser":{"read":true}}"#), Some("u1"));
assert_eq!(principals(acl), (vec!["u1".to_string()], Vec::new()));
}
#[test]
fn an_anonymous_create_loses_the_current_user_entry_rather_than_keeping_it() {
let acl = default_acl_for_create(
&declared(r#"{"currentUser":{"read":true,"write":true}}"#),
None,
);
let ParseValue::Object(map) = &acl else {
panic!("expected an object")
};
assert!(map.is_empty(), "the literal key must not survive: {map:?}");
assert_eq!(principals(acl), (Vec::new(), Vec::new()));
}
#[test]
fn other_entries_survive_and_the_caller_is_appended() {
let acl = default_acl_for_create(
&declared(
r#"{"role:Admins":{"read":true,"write":true},"currentUser":{"read":true},"*":{"read":true}}"#,
),
Some("u1"),
);
let (read, write) = principals(acl);
assert_eq!(read, vec!["role:Admins", "*", "u1"]);
assert_eq!(write, vec!["role:Admins"]);
}
#[test]
fn a_caller_already_named_keeps_its_position() {
let acl = default_acl_for_create(
&declared(
r#"{"u1":{"read":true},"*":{"read":true},"currentUser":{"read":true,"write":true}}"#,
),
Some("u1"),
);
let (read, write) = principals(acl);
assert_eq!(read, vec!["u1", "*"]);
assert_eq!(write, vec!["u1"], "the currentUser entry replaced it");
}
#[test]
fn a_falsy_current_user_entry_is_left_alone() {
let acl = default_acl_for_create(&declared(r#"{"currentUser":null}"#), Some("u1"));
let ParseValue::Object(map) = &acl else {
panic!("expected an object")
};
assert!(map.contains_key("currentUser"));
assert!(!map.contains_key("u1"));
}
#[test]
fn a_truthy_non_object_declaration_yields_a_master_only_row() {
let acl = default_acl_for_create(&declared(r#""nonsense""#), Some("u1"));
assert!(matches!(&acl, ParseValue::String(s) if s == "nonsense"));
let mut carrier = ParseMap::new();
carrier.insert("ACL".to_string(), acl);
let lowered = lower_acl(carrier);
for column in ["_rperm", "_wperm"] {
assert!(matches!(lowered.get(column), Some(ParseValue::Array(a)) if a.is_empty()));
}
}
#[test]
fn a_row_with_no_acl_gets_no_columns_and_no_acl_key_back() {
let lowered = lower_acl(row(vec![("title", ParseValue::String("x".into()))]));
assert!(lowered.get("_rperm").is_none());
let raised = raise_acl(lowered);
assert!(
raised.get("ACL").is_none(),
"absent columns produce no ACL key at all, not null and not an empty object"
);
}
}