use parse_rust_core::{
ClassLevelPermissions, ErrorCode, ErrorDetail, OpEntity, Operation, ParseError, ParseMap,
ParseValue, PfEntity, UserFieldsKey,
};
use parse_rust_storage::{ClassSchema, Comparison, Constraint, FieldType, Query, SortDirection};
use crate::acl::AclScope;
use crate::query_parse::ParsedWhere;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteAction {
Create,
Update,
}
#[derive(Debug, Clone)]
pub struct PermissionOptions {
pub protected_fields_owner_exempt: bool,
pub error_detail: ErrorDetail,
pub allow_client_class_creation: bool,
}
impl Default for PermissionOptions {
fn default() -> Self {
Self {
protected_fields_owner_exempt: true,
allow_client_class_creation: false,
error_detail: ErrorDetail::Withheld,
}
}
}
pub fn test_permissions(
clp: Option<&ClassLevelPermissions>,
acl_group: &[String],
operation: Operation,
) -> bool {
let Some(clp) = clp else { return true };
let Some(perm) = clp.op(operation) else {
return true;
};
if perm.grants(&OpEntity::Public) {
return true;
}
acl_group.iter().any(|a| perm.grants(&OpEntity::parse(a)))
}
pub fn validate_permission(
clp: Option<&ClassLevelPermissions>,
class_name: &str,
acl_group: &[String],
operation: Operation,
action: Option<WriteAction>,
detail: ErrorDetail,
) -> Result<(), ParseError> {
if test_permissions(clp, acl_group, operation) {
return Ok(());
}
let Some(clp) = clp else { return Ok(()) };
let Some(perm) = clp.op(operation) else {
return Ok(());
};
if perm.grants(&OpEntity::RequiresAuthentication) {
let anonymous = acl_group.is_empty() || acl_group == ["*"];
if anonymous {
return Err(ParseError::permission_denied(
ErrorCode::ObjectNotFound,
"Permission denied, user needs to be authenticated.",
detail,
));
}
return Ok(());
}
if operation.user_fields_key() == UserFieldsKey::Write && operation == Operation::Create {
return Err(forbidden(class_name, operation, detail));
}
if !clp.user_fields(operation).is_empty() {
return Ok(());
}
if !perm.pointer_fields.is_empty()
&& (operation != Operation::AddField || action == Some(WriteAction::Update))
{
return Ok(());
}
Err(forbidden(class_name, operation, detail))
}
fn forbidden(class_name: &str, operation: Operation, detail: ErrorDetail) -> ParseError {
ParseError::permission_denied(
ErrorCode::OperationForbidden,
format!(
"Permission denied for action {} on class {class_name}.",
operation.as_key()
),
detail,
)
}
#[must_use]
#[derive(Debug)]
pub enum PointerPermOutcome {
Unconstrained,
Constrained(Query),
DenyAll,
}
pub fn apply_pointer_permissions(
schema: &ClassSchema,
clp: Option<&ClassLevelPermissions>,
operation: Operation,
acl_group: &[String],
query: &Query,
) -> Result<PointerPermOutcome, ParseError> {
if test_permissions(clp, acl_group, operation) {
return Ok(PointerPermOutcome::Unconstrained);
}
let Some(clp) = clp else {
return Ok(PointerPermOutcome::Unconstrained);
};
let fields = clp.applicable_pointer_fields(operation);
if fields.is_empty() {
return Ok(PointerPermOutcome::Unconstrained);
}
let user_acl: Vec<&String> = acl_group
.iter()
.filter(|a| !a.starts_with("role:") && a.as_str() != "*")
.collect();
let [user_id] = user_acl.as_slice() else {
return Ok(PointerPermOutcome::DenyAll);
};
let user_pointer = ParseValue::Pointer {
class_name: "_User".to_string(),
object_id: (*user_id).clone(),
};
let mut alternatives = Vec::with_capacity(fields.len());
for field in &fields {
let constraint = match schema.field(field) {
Some(FieldType::Pointer { .. }) | Some(FieldType::Object) => {
Constraint::equal(field.clone(), user_pointer.clone())
}
Some(FieldType::Array) => Constraint {
field: field.clone(),
comparison: Comparison::All(vec![user_pointer.clone()]),
},
_ => {
let class_name = &schema.class_name;
return Err(ParseError::internal(format!(
"An unexpected condition occurred when resolving pointer permissions: \
{class_name} {field}"
)));
}
};
alternatives.push(Query::from_constraints(vec![constraint]));
}
let mut out = query.clone();
out.conjoin(Query::any_of(alternatives));
Ok(PointerPermOutcome::Constrained(out))
}
pub fn adds_field<'a>(
schema: &ClassSchema,
class_exists: bool,
keys: impl IntoIterator<Item = &'a str>,
is_delete: impl Fn(&str) -> bool,
) -> bool {
if !class_exists {
return false;
}
keys.into_iter().any(|key| {
if is_delete(key) {
return false;
}
let root = key.split('.').next().unwrap_or(key);
schema.field(root).is_none()
})
}
#[derive(Debug, Clone, Default)]
pub struct ProtectedFieldPlan {
pub strip: Vec<String>,
pub user_field_rules: Vec<(String, Vec<String>)>,
}
impl ProtectedFieldPlan {
pub fn is_empty(&self) -> bool {
self.strip.is_empty() && self.user_field_rules.is_empty()
}
}
pub fn plan_protected_fields(
class_name: &str,
clp: Option<&ClassLevelPermissions>,
scope: &AclScope,
pinned_object_id: Option<&str>,
options: &PermissionOptions,
) -> Option<ProtectedFieldPlan> {
let clp = clp?;
if clp.protected_fields().is_empty() {
return None;
}
let acl_group = scope.acl_group();
if class_name == "_User"
&& options.protected_fields_owner_exempt
&& pinned_object_id.is_some_and(|id| acl_group.iter().any(|a| a == id))
{
return None;
}
let authenticated = scope.user_id().is_some();
let mut sets: Vec<&Vec<String>> = Vec::new();
let mut user_field_rules = Vec::new();
for (entity, fields) in clp.protected_fields() {
match entity {
PfEntity::UserField(name) => user_field_rules.push((name.clone(), fields.clone())),
PfEntity::Public => sets.push(fields),
PfEntity::Authenticated if authenticated => sets.push(fields),
PfEntity::Role(name) if authenticated && scope.has_role(name) => sets.push(fields),
_ => {}
}
}
if let Some(user_id) = scope.user_id() {
if let Some(fields) = clp
.protected_fields()
.get(&PfEntity::User(user_id.to_string()))
{
sets.push(fields);
}
}
Some(ProtectedFieldPlan {
strip: intersect_all(&sets),
user_field_rules,
})
}
fn intersect_all(sets: &[&Vec<String>]) -> Vec<String> {
let Some((first, rest)) = sets.split_first() else {
return Vec::new();
};
let mut out: Vec<String> = Vec::new();
for field in first.iter() {
if out.contains(field) {
continue;
}
if rest.iter().all(|set| set.contains(field)) {
out.push(field.clone());
}
}
out
}
pub fn deny_protected_fields(
plan: Option<&ProtectedFieldPlan>,
class_name: &str,
where_: &ParsedWhere,
order: &[(String, SortDirection)],
detail: ErrorDetail,
) -> Result<(), ParseError> {
let Some(plan) = plan else { return Ok(()) };
if plan.strip.is_empty() {
return Ok(());
}
let denied = |key: &str| -> bool {
let root = key.split('.').next().unwrap_or(key);
plan.strip.iter().any(|f| f == key || f == root)
};
for key in where_.field_keys() {
if denied(&key) {
return Err(ParseError::permission_denied(
ErrorCode::OperationForbidden,
format!("This user is not allowed to query {key} on class {class_name}"),
detail,
));
}
}
for (key, _) in order {
if denied(key) {
return Err(ParseError::permission_denied(
ErrorCode::OperationForbidden,
format!("This user is not allowed to sort by {key} on class {class_name}"),
detail,
));
}
}
Ok(())
}
pub fn filter_sensitive_data(
row: &mut ParseMap,
class_name: &str,
scope: &AclScope,
plan: Option<&ProtectedFieldPlan>,
is_read: bool,
options: &PermissionOptions,
) {
let is_user_class = class_name == "_User";
let acl_group = scope.acl_group();
let mut protected: Option<Vec<String>> = plan.map(|p| p.strip.clone());
if is_read {
if let Some(plan) = plan {
let matched: Vec<&Vec<String>> = plan
.user_field_rules
.iter()
.filter(|(field, _)| row_field_points_at(row.get(field), scope.user_id()))
.map(|(_, fields)| fields)
.collect();
if !matched.is_empty() {
let mut sets = matched;
if let Some(existing) = protected.as_ref() {
sets.push(existing);
}
protected = Some(intersect_all(&sets));
}
}
}
if is_user_class {
row.shift_remove("sessionToken");
}
let owner_exempt = options.protected_fields_owner_exempt
&& is_user_class
&& scope.user_id().is_some_and(
|uid| matches!(row.get("objectId"), Some(ParseValue::String(id)) if id == uid),
);
if !owner_exempt {
if let Some(fields) = protected {
for field in fields {
row.shift_remove(&field);
}
}
}
crate::guard::strip_internal_keys(row);
if !is_user_class || scope.is_master() {
return;
}
let own_row =
matches!(row.get("objectId"), Some(ParseValue::String(id)) if acl_group.contains(id));
if !own_row {
row.shift_remove("authData");
}
}
fn row_field_points_at(value: Option<&ParseValue>, user_id: Option<&str>) -> bool {
let Some(user_id) = user_id else { return false };
match value {
Some(ParseValue::Pointer { object_id, .. }) => object_id == user_id,
Some(ParseValue::Array(items)) => items
.iter()
.any(|v| matches!(v, ParseValue::Pointer { object_id, .. } if object_id == user_id)),
Some(ParseValue::Object(map)) => {
matches!(map.get("objectId"), Some(ParseValue::String(id)) if id == user_id)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use parse_rust_core::ClassLevelPermissions;
fn clp(json: &str) -> ClassLevelPermissions {
let value = parse_rust_core::classify(
serde_json::from_str(json).expect("test literal must be valid JSON"),
)
.expect("classify");
match value {
ParseValue::Object(m) => ClassLevelPermissions::from_map(m),
_ => panic!("expected an object"),
}
}
fn user_scope() -> AclScope {
AclScope::user("u1", vec![]).expect("scope")
}
const WITHHELD: ErrorDetail = ErrorDetail::Withheld;
const DISCLOSED: ErrorDetail = ErrorDetail::Disclosed;
#[test]
fn no_clp_at_all_allows_every_operation() {
for op in Operation::ALL {
assert!(test_permissions(None, &["*".to_string()], op), "{op:?}");
assert!(
validate_permission(None, "Post", &["*".to_string()], op, None, DISCLOSED).is_ok()
);
}
}
#[test]
fn an_absent_operation_entry_is_unrestricted() {
let c = clp(r#"{"find":{}}"#);
assert!(
validate_permission(
Some(&c),
"Post",
&["*".into()],
Operation::Update,
None,
DISCLOSED
)
.is_ok(),
"update has no entry, so it is unrestricted"
);
let e = validate_permission(
Some(&c),
"Post",
&["*".into()],
Operation::Find,
None,
DISCLOSED,
)
.unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
assert_eq!(
e.message,
"Permission denied for action find on class Post."
);
let withheld = validate_permission(
Some(&c),
"Post",
&["*".into()],
Operation::Find,
None,
WITHHELD,
)
.unwrap_err();
assert_eq!(withheld.code, ErrorCode::OperationForbidden);
assert_eq!(withheld.message, "Permission denied");
}
#[test]
fn a_granted_role_passes_the_gate() {
let c = clp(r#"{"find":{"role:Admins":true}}"#);
let member = AclScope::user("u1", vec!["Admins".into()]).expect("scope");
assert!(validate_permission(
Some(&c),
"Post",
&member.acl_group(),
Operation::Find,
None,
WITHHELD
)
.is_ok());
let outsider = AclScope::user("u2", vec!["Others".into()]).expect("scope");
assert!(validate_permission(
Some(&c),
"Post",
&outsider.acl_group(),
Operation::Find,
None,
WITHHELD
)
.is_err());
}
#[test]
fn requires_authentication_denies_anonymously_with_object_not_found() {
let c = clp(r#"{"find":{"requiresAuthentication":true}}"#);
let e = validate_permission(
Some(&c),
"Post",
&AclScope::Anonymous.acl_group(),
Operation::Find,
None,
DISCLOSED,
)
.unwrap_err();
assert_eq!(e.code, ErrorCode::ObjectNotFound);
assert_eq!(
e.message,
"Permission denied, user needs to be authenticated."
);
let withheld = validate_permission(
Some(&c),
"Post",
&AclScope::Anonymous.acl_group(),
Operation::Find,
None,
WITHHELD,
)
.unwrap_err();
assert_eq!(withheld.code, ErrorCode::ObjectNotFound);
assert_eq!(withheld.message, "Permission denied");
assert_eq!(
validate_permission(Some(&c), "Post", &[], Operation::Find, None, WITHHELD)
.unwrap_err()
.code,
ErrorCode::ObjectNotFound
);
assert!(validate_permission(
Some(&c),
"Post",
&user_scope().acl_group(),
Operation::Find,
None,
WITHHELD
)
.is_ok());
}
#[test]
fn requires_authentication_with_pointer_fields_still_narrows_in_stage_two() {
let c = clp(r#"{"find":{"requiresAuthentication":true,"pointerFields":["owner"]}}"#);
let scope = user_scope();
assert!(validate_permission(
Some(&c),
"Post",
&scope.acl_group(),
Operation::Find,
None,
WITHHELD
)
.is_ok());
let schema = ClassSchema::new("Post").with_field(
"owner",
FieldType::Pointer {
target_class: "_User".into(),
},
);
match apply_pointer_permissions(
&schema,
Some(&c),
Operation::Find,
&scope.acl_group(),
&Query::new(),
)
.expect("no misconfiguration")
{
PointerPermOutcome::Constrained(q) => assert_eq!(q.clauses.len(), 1),
other => panic!("expected Constrained, got {other:?}"),
}
}
#[test]
fn write_user_fields_lock_down_create_only() {
let c = clp(r#"{"create":{},"update":{},"writeUserFields":["owner"]}"#);
let scope = user_scope();
let e = validate_permission(
Some(&c),
"Post",
&scope.acl_group(),
Operation::Create,
Some(WriteAction::Create),
DISCLOSED,
)
.unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
assert_eq!(
e.message,
"Permission denied for action create on class Post."
);
assert_eq!(
validate_permission(
Some(&c),
"Post",
&scope.acl_group(),
Operation::Create,
Some(WriteAction::Create),
WITHHELD,
)
.unwrap_err()
.message,
"Permission denied"
);
assert!(validate_permission(
Some(&c),
"Post",
&scope.acl_group(),
Operation::Update,
Some(WriteAction::Update),
WITHHELD
)
.is_ok());
}
#[test]
fn add_field_defers_only_on_update() {
let c = clp(r#"{"addField":{"pointerFields":["owner"]}}"#);
let scope = user_scope();
assert!(validate_permission(
Some(&c),
"Post",
&scope.acl_group(),
Operation::AddField,
Some(WriteAction::Update),
WITHHELD
)
.is_ok());
assert_eq!(
validate_permission(
Some(&c),
"Post",
&scope.acl_group(),
Operation::AddField,
Some(WriteAction::Create),
WITHHELD
)
.unwrap_err()
.code,
ErrorCode::OperationForbidden
);
}
#[test]
fn a_public_class_is_never_pointer_restricted() {
let c = clp(r#"{"find":{"*":true,"pointerFields":["owner"]}}"#);
let schema = ClassSchema::new("Post");
assert!(matches!(
apply_pointer_permissions(
&schema,
Some(&c),
Operation::Find,
&AclScope::Anonymous.acl_group(),
&Query::new()
)
.expect("ok"),
PointerPermOutcome::Unconstrained
));
}
#[test]
fn an_anonymous_caller_is_denied_all_by_a_pointer_permission() {
let c = clp(r#"{"find":{"pointerFields":["owner"]}}"#);
let schema = ClassSchema::new("Post").with_field(
"owner",
FieldType::Pointer {
target_class: "_User".into(),
},
);
assert!(matches!(
apply_pointer_permissions(
&schema,
Some(&c),
Operation::Find,
&AclScope::Anonymous.acl_group(),
&Query::new()
)
.expect("ok"),
PointerPermOutcome::DenyAll
));
}
#[test]
fn the_clause_shape_follows_the_schema_type() {
let c = clp(r#"{"find":{"pointerFields":["owner","editors","meta"]}}"#);
let schema = ClassSchema::new("Post")
.with_field(
"owner",
FieldType::Pointer {
target_class: "_User".into(),
},
)
.with_field("editors", FieldType::Array)
.with_field("meta", FieldType::Object);
let scope = user_scope();
let q = match apply_pointer_permissions(
&schema,
Some(&c),
Operation::Find,
&scope.acl_group(),
&Query::new(),
)
.expect("ok")
{
PointerPermOutcome::Constrained(q) => q,
other => panic!("expected Constrained, got {other:?}"),
};
match q.clauses.as_slice() {
[parse_rust_storage::Clause::Or(alts)] => {
assert_eq!(alts.len(), 3);
assert!(matches!(
alts[1].clauses[0],
parse_rust_storage::Clause::Field(Constraint {
comparison: Comparison::All(_),
..
})
));
}
other => panic!("expected a single Or clause, got {other:?}"),
}
}
#[test]
fn a_client_constraint_on_the_permission_field_survives_composition() {
let c = clp(r#"{"find":{"pointerFields":["owner"]}}"#);
let schema = ClassSchema::new("Post").with_field(
"owner",
FieldType::Pointer {
target_class: "_User".into(),
},
);
let scope = user_scope();
let client = Query::from_constraints(vec![Constraint::equal(
"owner",
ParseValue::Pointer {
class_name: "_User".into(),
object_id: "u1".into(),
},
)]);
let q = match apply_pointer_permissions(
&schema,
Some(&c),
Operation::Find,
&scope.acl_group(),
&client,
)
.expect("ok")
{
PointerPermOutcome::Constrained(q) => q,
other => panic!("expected Constrained, got {other:?}"),
};
assert!(matches!(
q.clauses.as_slice(),
[
parse_rust_storage::Clause::Field(f),
parse_rust_storage::Clause::And(nested)
] if f.field == "owner" && nested.len() == 1
));
}
#[test]
fn a_pointer_permission_on_an_unusable_field_type_is_a_500() {
let c = clp(r#"{"find":{"pointerFields":["title"]}}"#);
let schema = ClassSchema::new("Post").with_field("title", FieldType::String);
let e = apply_pointer_permissions(
&schema,
Some(&c),
Operation::Find,
&user_scope().acl_group(),
&Query::new(),
)
.unwrap_err();
assert_eq!(e.code, ErrorCode::InternalServerError);
assert_eq!(e.origin, parse_rust_core::ErrorOrigin::Internal);
assert!(e.message.contains("Post"));
assert!(e.message.contains("title"));
let c2 = clp(r#"{"find":{"pointerFields":["nope"]}}"#);
assert_eq!(
apply_pointer_permissions(
&ClassSchema::new("Post"),
Some(&c2),
Operation::Find,
&user_scope().acl_group(),
&Query::new()
)
.unwrap_err()
.code,
ErrorCode::InternalServerError
);
}
#[test]
fn protected_fields_intersect_rather_than_union() {
let c = clp(
r#"{"protectedFields":{"*":["email","phone","ssn"],"authenticated":["email","phone"],"role:A":["email"]}}"#,
);
let anon = plan_protected_fields(
"Post",
Some(&c),
&AclScope::Anonymous,
None,
&PermissionOptions::default(),
)
.expect("plan");
assert_eq!(anon.strip, vec!["email", "phone", "ssn"]);
let user = AclScope::user("u1", vec![]).expect("scope");
let plan =
plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
.expect("plan");
assert_eq!(plan.strip, vec!["email", "phone"]);
let admin = AclScope::user("u1", vec!["A".into()]).expect("scope");
let plan = plan_protected_fields(
"Post",
Some(&c),
&admin,
None,
&PermissionOptions::default(),
)
.expect("plan");
assert_eq!(plan.strip, vec!["email"]);
}
#[test]
fn an_inapplicable_role_contributes_nothing() {
let c = clp(r#"{"protectedFields":{"*":["email"],"role:A":["phone"]}}"#);
let outsider = AclScope::user("u1", vec!["B".into()]).expect("scope");
let plan = plan_protected_fields(
"Post",
Some(&c),
&outsider,
None,
&PermissionOptions::default(),
)
.expect("plan");
assert_eq!(
plan.strip,
vec!["email"],
"a role the caller does not hold must not join the intersection"
);
}
#[test]
fn the_user_owner_exemption_defaults_to_exempt_and_honors_the_option() {
let c = clp(r#"{"protectedFields":{"*":["email"]}}"#);
let user = AclScope::user("u1", vec![]).expect("scope");
assert!(
plan_protected_fields(
"_User",
Some(&c),
&user,
Some("u1"),
&PermissionOptions::default()
)
.is_none(),
"an unset option means exempt"
);
let strict = PermissionOptions {
protected_fields_owner_exempt: false,
..PermissionOptions::default()
};
assert!(plan_protected_fields("_User", Some(&c), &user, Some("u1"), &strict).is_some());
assert!(plan_protected_fields(
"_User",
Some(&c),
&user,
Some("u2"),
&PermissionOptions::default()
)
.is_some());
}
#[test]
fn a_user_field_rule_matching_the_row_reduces_what_is_stripped() {
let c = clp(r#"{"protectedFields":{"*":["email","phone"],"userField:owner":["phone"]}}"#);
let user = AclScope::user("u1", vec![]).expect("scope");
let plan =
plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
.expect("plan");
assert_eq!(plan.strip, vec!["email", "phone"]);
assert_eq!(plan.user_field_rules.len(), 1);
let mut row = ParseMap::new();
row.insert("objectId".into(), ParseValue::String("p1".into()));
row.insert(
"owner".into(),
ParseValue::Pointer {
class_name: "_User".into(),
object_id: "u1".into(),
},
);
row.insert("email".into(), ParseValue::String("a@b.c".into()));
row.insert("phone".into(), ParseValue::String("555".into()));
filter_sensitive_data(
&mut row,
"Post",
&user,
Some(&plan),
true,
&PermissionOptions::default(),
);
assert!(row.get("email").is_some());
assert!(row.get("phone").is_none());
let mut other = ParseMap::new();
other.insert(
"owner".into(),
ParseValue::Pointer {
class_name: "_User".into(),
object_id: "u2".into(),
},
);
other.insert("email".into(), ParseValue::String("a@b.c".into()));
other.insert("phone".into(), ParseValue::String("555".into()));
filter_sensitive_data(
&mut other,
"Post",
&user,
Some(&plan),
true,
&PermissionOptions::default(),
);
assert!(other.get("phone").is_none());
assert!(
other.get("email").is_none(),
"no rule matched, so the pre-query list stands and both are stripped"
);
}
#[test]
fn a_user_field_rule_that_cannot_be_evaluated_protects_more_not_less() {
let c = clp(r#"{"protectedFields":{"*":["email","phone"],"userField:owner":["phone"]}}"#);
let user = AclScope::user("u1", vec![]).expect("scope");
let plan =
plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
.expect("plan");
let mut projected = ParseMap::new();
projected.insert("email".into(), ParseValue::String("a@b.c".into()));
projected.insert("phone".into(), ParseValue::String("555".into()));
filter_sensitive_data(
&mut projected,
"Post",
&user,
Some(&plan),
true,
&PermissionOptions::default(),
);
assert!(
projected.get("phone").is_none(),
"the rule cannot match without `owner`, so `phone` stays protected"
);
assert!(
projected.get("email").is_none(),
"and so does `email`: the unreduced list is the one that applies"
);
let mut full = ParseMap::new();
full.insert(
"owner".into(),
ParseValue::Pointer {
class_name: "_User".into(),
object_id: "u1".into(),
},
);
full.insert("email".into(), ParseValue::String("a@b.c".into()));
full.insert("phone".into(), ParseValue::String("555".into()));
filter_sensitive_data(
&mut full,
"Post",
&user,
Some(&plan),
true,
&PermissionOptions::default(),
);
assert!(full.get("email").is_some());
}
#[test]
fn a_principal_shaped_role_name_is_not_unwrapped() {
let scope = AclScope::user("u1", vec!["role:Admin".to_string()]).expect("scope");
let group = scope.acl_group();
assert!(
group.iter().any(|g| g == "role:role:Admin"),
"the name is prefixed once, not collapsed: {group:?}"
);
assert!(
!group.iter().any(|g| g == "role:Admin"),
"holding a role named `role:Admin` must not grant `Admin`: {group:?}"
);
let doubled = clp(r#"{"find":{"role:role:Admin":true}}"#);
assert!(test_permissions(
Some(&doubled),
&group,
parse_rust_core::Operation::Find
));
let single = clp(r#"{"find":{"role:Admin":true}}"#);
assert!(!test_permissions(
Some(&single),
&group,
parse_rust_core::Operation::Find
));
}
#[test]
fn auth_data_survives_only_for_master_and_the_row_owner() {
let mut row = ParseMap::new();
row.insert("objectId".into(), ParseValue::String("u1".into()));
row.insert("authData".into(), ParseValue::Object(ParseMap::new()));
let owner = AclScope::user("u1", vec![]).expect("scope");
filter_sensitive_data(
&mut row,
"_User",
&owner,
None,
true,
&PermissionOptions::default(),
);
assert!(row.get("authData").is_some());
let mut row = ParseMap::new();
row.insert("objectId".into(), ParseValue::String("u1".into()));
row.insert("authData".into(), ParseValue::Object(ParseMap::new()));
let stranger = AclScope::user("u2", vec![]).expect("scope");
filter_sensitive_data(
&mut row,
"_User",
&stranger,
None,
true,
&PermissionOptions::default(),
);
assert!(row.get("authData").is_none());
}
#[test]
fn the_password_hash_never_reaches_a_response_under_any_name() {
let mut row = ParseMap::new();
row.insert("objectId".into(), ParseValue::String("u1".into()));
row.insert("_hashed_password".into(), ParseValue::String("hash".into()));
row.insert("sessionToken".into(), ParseValue::String("r:t".into()));
filter_sensitive_data(
&mut row,
"_User",
&AclScope::user("u1", vec![]).expect("scope"),
None,
true,
&PermissionOptions::default(),
);
assert!(row.get("_hashed_password").is_none());
assert!(
row.get("password").is_none(),
"the hash is never rehydrated under a user-facing name"
);
assert!(row.get("sessionToken").is_none());
}
#[test]
fn adds_field_ignores_deletes_and_dotted_roots() {
let schema = ClassSchema::new("Post").with_field("meta", FieldType::Object);
assert!(!adds_field(&schema, true, ["meta.x"], |_| false));
assert!(adds_field(&schema, true, ["fresh"], |_| false));
assert!(!adds_field(&schema, true, ["fresh"], |k| k == "fresh"));
assert!(
!adds_field(&schema, false, ["fresh"], |_| false),
"a write that creates the class never runs the addField gate"
);
}
}