use parse_rust_auth::{create_session, CreatedWith, NewSession};
use parse_rust_core::{ErrorCode, FieldWrite, ParseError, ParseMap, ParseValue};
use parse_rust_rest::{FindOptions, WriteBody};
use parse_rust_storage::{Constraint, Query, QueryOptions, StorageAdapter};
use serde_json::{json, Value as Json};
use crate::auth::Authority;
use crate::request::RequestContext;
use crate::state::AppState;
pub const USER_CLASS: &str = "_User";
const HASHED_PASSWORD: &str = "_hashed_password";
fn strip_sensitive(mut row: ParseMap) -> ParseMap {
for key in [
HASHED_PASSWORD,
"password",
"_perishable_token",
"_email_verify_token",
] {
row.shift_remove(key);
}
row
}
fn body_has_truthy(body: &WriteBody, key: &str) -> bool {
match body.get(key) {
Some(FieldWrite::Value(v)) => parse_rust_core::is_js_truthy(v),
Some(FieldWrite::Op(_)) => true,
None => false,
}
}
fn take_string(body: &WriteBody, key: &str) -> Option<String> {
match body.get(key) {
Some(FieldWrite::Value(ParseValue::String(s))) => Some(s.clone()),
_ => None,
}
}
pub(crate) async fn prepare_user_write(
body: &mut WriteBody,
is_create: bool,
) -> Result<(), ParseError> {
hash_user_password(body).await?;
if is_create {
ensure_user_identity_and_acl(body)?;
}
Ok(())
}
async fn hash_user_password(body: &mut WriteBody) -> Result<(), ParseError> {
let Some(password) = take_string(body, "password") else {
return Ok(());
};
let hash = parse_rust_auth::password::hash(password).await?;
body.shift_remove("password");
body.insert(
HASHED_PASSWORD.to_string(),
FieldWrite::Value(ParseValue::String(hash)),
);
Ok(())
}
pub(crate) fn ensure_user_identity_and_acl(body: &mut WriteBody) -> Result<String, ParseError> {
if let Some(write) = body.get("objectId") {
let truthy_non_string = match write {
FieldWrite::Value(ParseValue::String(_)) => false,
FieldWrite::Value(v) => parse_rust_core::is_js_truthy(v),
FieldWrite::Op(_) => true,
};
if truthy_non_string {
let got = match write {
FieldWrite::Value(v) => parse_rust_schema::infer_type(v),
FieldWrite::Op(op) => parse_rust_schema::infer_op_type(op)?,
};
return Err(match got {
Some(got) => parse_rust_schema::infer::schema_mismatch(
USER_CLASS,
"objectId",
&parse_rust_storage::FieldType::String,
&got,
),
None => ParseError::invalid_json("objectId is an invalid field name."),
});
}
}
let existing = match body.get("objectId") {
Some(FieldWrite::Value(ParseValue::String(s))) if !s.is_empty() => Some(s.clone()),
_ => None,
};
let object_id = match existing {
Some(id) => id,
None => {
let id = parse_rust_core::new_object_id();
body.insert(
"objectId".to_string(),
FieldWrite::Value(ParseValue::String(id.clone())),
);
id
}
};
let mut permissions = ParseMap::new();
permissions.insert("read".to_string(), ParseValue::Bool(true));
permissions.insert("write".to_string(), ParseValue::Bool(true));
enum Shape {
Replace,
Merge,
LeaveAlone,
}
let shape = match body.get("ACL") {
None => Shape::Replace,
Some(FieldWrite::Value(v)) if !parse_rust_core::is_js_truthy(v) => Shape::Replace,
Some(FieldWrite::Value(ParseValue::Object(_))) => Shape::Merge,
Some(FieldWrite::Value(
ParseValue::Bool(_) | ParseValue::Number(_) | ParseValue::String(_),
)) => Shape::LeaveAlone,
Some(FieldWrite::Op(_)) | Some(FieldWrite::Value(_)) => Shape::Replace,
};
if matches!(shape, Shape::Replace) {
let mut acl = ParseMap::new();
acl.insert(object_id.clone(), ParseValue::Object(permissions));
body.insert(
"ACL".to_string(),
FieldWrite::Value(ParseValue::Object(acl)),
);
return Ok(object_id);
}
if let (Shape::Merge, Some(FieldWrite::Value(ParseValue::Object(acl)))) =
(shape, body.get_mut("ACL"))
{
acl.insert(object_id.clone(), ParseValue::Object(permissions));
}
Ok(object_id)
}
pub(crate) fn reject_role_prefixed_object_id(
body: &WriteBody,
rc: &RequestContext,
) -> Result<(), ParseError> {
let Some(FieldWrite::Value(ParseValue::String(id))) = body.get("objectId") else {
return Ok(());
};
if !id.starts_with("role:") {
return Ok(());
}
Err(ParseError::permission_denied(
ErrorCode::OperationForbidden,
"Invalid object ID.",
rc.options.error_detail,
))
}
pub(crate) fn require_create_credentials(body: &WriteBody) -> Result<(), ParseError> {
if take_string(body, "username")
.filter(|u| !u.is_empty())
.is_none()
{
return Err(ParseError::new(
ErrorCode::UsernameMissing,
"bad or missing username",
));
}
if take_string(body, "password")
.filter(|p| !p.is_empty())
.is_none()
{
return Err(ParseError::new(
ErrorCode::PasswordMissing,
"password is required",
));
}
Ok(())
}
pub async fn signup_core(
state: &AppState,
rc: &RequestContext,
authority: &Authority,
body: &Json,
) -> Result<Json, ParseError> {
let mut body = parse_rust_rest::decode_write_body(body, parse_rust_core::op::OpPath::Create)?;
parse_rust_rest::reject_reserved_keys_in(body.keys().map(String::as_str))?;
parse_rust_rest::enforce_object_id_policy(&body, state.config().allow_custom_object_id)?;
reject_role_prefixed_object_id(&body, rc)?;
reject_client_restricted_user_fields(&body, rc, authority)?;
require_create_credentials(&body)?;
if !authority.is_privileged() {
parse_rust_rest::validate_permission(
rc.snapshot.clp(USER_CLASS),
USER_CLASS,
&rc.scope.acl_group(),
parse_rust_core::Operation::Create,
None,
rc.options.error_detail,
)?;
}
validate_user_identity(state, rc, &body, "").await?;
prepare_user_write(&mut body, true).await?;
let ctx = rc.ctx(state.storage());
let created = parse_rust_rest::create(&ctx, USER_CLASS, body)
.await
.map_err(map_duplicate)?;
let session = create_session(
state.storage(),
&state.config().session,
NewSession {
user_object_id: &created.object_id,
created_with: Some(CreatedWith::signup(None)),
installation_id: rc.installation_id.as_deref(),
},
)
.await?;
Ok(json!({
"objectId": created.object_id,
"createdAt": created.created_at.to_iso(),
"sessionToken": session.session_token,
}))
}
pub(crate) fn map_duplicate(e: ParseError) -> ParseError {
if e.code != ErrorCode::DuplicateValue {
return e;
}
match e.duplicated_field() {
Some("username") => ParseError::new(
ErrorCode::UsernameTaken,
"Account already exists for this username.",
),
Some("email") => ParseError::new(
ErrorCode::EmailTaken,
"Account already exists for this email address.",
),
_ => e,
}
}
pub async fn login_core(
state: &AppState,
rc: &RequestContext,
authority: &Authority,
body: &Json,
) -> Result<Json, ParseError> {
let body = parse_rust_rest::decode_write_body(body, parse_rust_core::op::OpPath::Create)?;
let has_username = body_has_truthy(&body, "username");
let has_email = body_has_truthy(&body, "email");
if !has_username && !has_email {
return Err(ParseError::new(
ErrorCode::UsernameMissing,
"username/email is required.",
));
}
if !body_has_truthy(&body, "password") {
return Err(ParseError::new(
ErrorCode::PasswordMissing,
"password is required.",
));
}
let invalid_credentials =
|| ParseError::new(ErrorCode::ObjectNotFound, "Invalid username/password.");
let username = take_string(&body, "username");
let email = take_string(&body, "email");
let Some(password) = take_string(&body, "password") else {
return Err(invalid_credentials());
};
if (has_username && username.is_none()) || (has_email && email.is_none()) {
return Err(invalid_credentials());
}
let schema = rc.snapshot.get_or_default(USER_CLASS);
let identifier = username.filter(|_| has_username);
let username_for_preference = identifier.clone();
let email = email.filter(|_| has_email);
let query = match (identifier, email) {
(Some(username), Some(email)) => Query::from_constraints(vec![
Constraint::equal("email", ParseValue::String(email)),
Constraint::equal("username", ParseValue::String(username)),
]),
(None, Some(email)) => {
Query::from_constraints(vec![Constraint::equal("email", ParseValue::String(email))])
}
(Some(identifier), None) => Query::any_of(vec![
Query::from_constraints(vec![Constraint::equal(
"username",
ParseValue::String(identifier.clone()),
)]),
Query::from_constraints(vec![Constraint::equal(
"email",
ParseValue::String(identifier),
)]),
]),
(None, None) => return Err(invalid_credentials()),
};
let rows = state
.storage()
.find(&schema, &query, &QueryOptions::default())
.await?;
let invalid = || ParseError::new(ErrorCode::ObjectNotFound, "Invalid username/password.");
let row = select_login_row(rows, username_for_preference.as_deref());
let Some(row) = row else {
parse_rust_auth::password::verify_dummy(password).await;
return Err(invalid());
};
let hash = match row.get(HASHED_PASSWORD) {
Some(ParseValue::String(hash)) if !hash.is_empty() => hash.clone(),
_ => {
parse_rust_auth::password::verify_dummy(password).await;
return Err(invalid());
}
};
if !parse_rust_auth::password::verify(password, hash).await {
return Err(invalid());
}
if !authority.is_master() && acl_is_explicitly_empty(&row) {
return Err(invalid());
}
let Some(ParseValue::String(object_id)) = row.get("objectId") else {
return Err(ParseError::internal("stored user has no objectId"));
};
let session = create_session(
state.storage(),
&state.config().session,
NewSession {
user_object_id: object_id,
created_with: Some(CreatedWith::login(None)),
installation_id: rc.installation_id.as_deref(),
},
)
.await?;
let object_id = object_id.to_string();
let mut out = refetch_for_response(state, rc, &object_id, row).await?;
out.insert(
"sessionToken".to_string(),
ParseValue::String(session.session_token),
);
Ok(crate::routes::classes::body_of(&out))
}
fn select_login_row(rows: Vec<ParseMap>, submitted_username: Option<&str>) -> Option<ParseMap> {
if rows.len() <= 1 {
return rows.into_iter().next();
}
let mut rows = rows;
let exact = submitted_username.and_then(|name| {
rows.iter()
.position(|r| matches!(r.get("username"), Some(ParseValue::String(u)) if u == name))
});
match exact {
Some(i) => Some(rows.swap_remove(i)),
None => rows.into_iter().next(),
}
}
async fn refetch_for_response(
state: &AppState,
rc: &RequestContext,
object_id: &str,
row: ParseMap,
) -> Result<ParseMap, ParseError> {
if rc.is_master() {
return Ok(parse_rust_rest::acl::raise_acl(strip_sensitive(row)));
}
let identity_only = || {
let mut map = ParseMap::new();
map.insert(
"objectId".to_string(),
ParseValue::String(object_id.to_string()),
);
map
};
let roles = parse_rust_auth::expand_roles(
state.storage(),
parse_rust_auth::RolePrincipal::User(object_id),
)
.await?;
let scope = parse_rust_rest::AclScope::user(
object_id.to_string(),
roles.iter().map(|r| r.as_str().to_string()).collect(),
)?;
let ctx = parse_rust_rest::Ctx::new(state.storage(), &rc.snapshot, &scope, &rc.options);
match parse_rust_rest::get(&ctx, USER_CLASS, object_id, FindOptions::default()).await {
Ok(row) => Ok(row),
Err(_) => Ok(identity_only()),
}
}
fn acl_is_explicitly_empty(row: &ParseMap) -> bool {
matches!(
parse_rust_rest::acl::raise_acl(row.clone()).get("ACL"),
Some(ParseValue::Object(acl)) if acl.is_empty()
)
}
pub async fn me_core(state: &AppState, rc: &RequestContext) -> Result<Json, ParseError> {
let invalid = || {
ParseError::permission_denied(
ErrorCode::InvalidSessionToken,
"Invalid session token",
rc.options.error_detail,
)
};
let (Some(token), Some(user_id)) = (rc.session_token.as_deref(), rc.user_id.as_deref()) else {
return Err(invalid());
};
let ctx = rc.ctx(state.storage());
let row = parse_rust_rest::get(&ctx, USER_CLASS, user_id, FindOptions::default())
.await
.map_err(|e| {
if e.code == ErrorCode::ObjectNotFound {
invalid()
} else {
e
}
})?;
let mut out = row;
out.insert(
"sessionToken".to_string(),
ParseValue::String(token.to_string()),
);
Ok(crate::routes::classes::body_of(&out))
}
pub async fn logout_core(state: &AppState, rc: &RequestContext) -> Result<Json, ParseError> {
if let Some(token) = rc.session_token.as_deref() {
parse_rust_auth::revoke(state.storage(), token).await?;
}
Ok(json!({}))
}
const CLIENT_FORBIDDEN_USER_FIELDS: [&str; 2] = ["emailVerified", "authData"];
fn forbidden_label(field: &str) -> &str {
match field {
"emailVerified" => "email verification",
other => other,
}
}
fn reject_client_restricted_user_fields(
body: &WriteBody,
rc: &RequestContext,
authority: &Authority,
) -> Result<(), ParseError> {
if authority.is_privileged() {
return Ok(());
}
for field in CLIENT_FORBIDDEN_USER_FIELDS {
if body.get(field).is_some() {
return Err(ParseError::permission_denied(
ErrorCode::OperationForbidden,
format!(
"Clients aren't allowed to manually update {}.",
forbidden_label(field)
),
rc.options.error_detail,
));
}
}
Ok(())
}
pub(crate) fn enforce_user_update_policy(
body: &WriteBody,
rc: &RequestContext,
authority: &Authority,
object_id: &str,
) -> Result<(), ParseError> {
if !authority.is_privileged() && rc.user_id.is_none() {
return Err(ParseError::permission_denied(
ErrorCode::SessionMissing,
format!("Cannot modify user {object_id}."),
rc.options.error_detail,
));
}
reject_client_restricted_user_fields(body, rc, authority)?;
match body.get("password") {
None | Some(FieldWrite::Value(ParseValue::String(_))) => {}
Some(_) => {
return Err(ParseError::incorrect_type(
"password must be a string".to_string(),
))
}
}
Ok(())
}
pub(crate) fn force_owner_into_acl(body: &mut WriteBody, object_id: &str, privileged: bool) {
if privileged {
return;
}
let mut permissions = ParseMap::new();
permissions.insert("read".to_string(), ParseValue::Bool(true));
permissions.insert("write".to_string(), ParseValue::Bool(true));
let owner_entry = ParseValue::Object(permissions);
let shape = match body.get("ACL") {
None => None,
Some(FieldWrite::Value(v)) if !parse_rust_core::is_js_truthy(v) => None,
Some(FieldWrite::Value(ParseValue::Object(_))) => Some(OwnerInto::ExistingMap),
Some(_) => Some(OwnerInto::FreshMap),
};
match shape {
None => {}
Some(OwnerInto::ExistingMap) => {
if let Some(FieldWrite::Value(ParseValue::Object(acl))) = body.get_mut("ACL") {
acl.insert(object_id.to_string(), owner_entry);
}
}
Some(OwnerInto::FreshMap) => {
let mut acl = ParseMap::new();
acl.insert(object_id.to_string(), owner_entry);
body.insert(
"ACL".to_string(),
FieldWrite::Value(ParseValue::Object(acl)),
);
}
}
}
enum OwnerInto {
ExistingMap,
FreshMap,
}
pub(crate) async fn validate_user_identity(
state: &AppState,
rc: &RequestContext,
body: &WriteBody,
object_id: &str,
) -> Result<(), ParseError> {
if matches!(body.get("username"), Some(FieldWrite::Op(_))) {
return Err(ParseError::invalid_json(
"You cannot use [object Object] as a query parameter.",
));
}
if let Some(FieldWrite::Value(ParseValue::String(username))) = body.get("username") {
if taken(state, rc, "username", username, object_id).await? {
return Err(ParseError::new(
ErrorCode::UsernameTaken,
"Account already exists for this username.",
));
}
}
let Some(FieldWrite::Value(ParseValue::String(email))) = body.get("email") else {
return Ok(());
};
if email.is_empty() {
return Ok(());
}
if !is_valid_email(email) {
return Err(ParseError::new(
ErrorCode::InvalidEmailAddress,
"Email address format is invalid.",
));
}
if taken(state, rc, "email", email, object_id).await? {
return Err(ParseError::new(
ErrorCode::EmailTaken,
"Account already exists for this email address.",
));
}
Ok(())
}
fn is_valid_email(email: &str) -> bool {
const LINE_TERMINATORS: [char; 4] = ['\n', '\r', '\u{2028}', '\u{2029}'];
if email.chars().any(|c| LINE_TERMINATORS.contains(&c)) {
return false;
}
let chars: Vec<char> = email.chars().collect();
chars.len() >= 3 && chars[1..chars.len() - 1].contains(&'@')
}
async fn taken(
state: &AppState,
rc: &RequestContext,
field: &str,
value: &str,
object_id: &str,
) -> Result<bool, ParseError> {
let query = Query::from_constraints(vec![
Constraint::equal(field, ParseValue::String(value.to_string())),
Constraint {
field: "objectId".to_string(),
comparison: parse_rust_storage::Comparison::NotEqual(ParseValue::String(
object_id.to_string(),
)),
},
]);
let schema = rc.snapshot.get_or_default(USER_CLASS);
let rows = state
.storage()
.find(
&schema,
&query,
&QueryOptions {
limit: Some(1),
case_insensitive: true,
..QueryOptions::default()
},
)
.await?;
Ok(!rows.is_empty())
}
#[cfg(test)]
mod tests {
fn row(username: &str, id: &str) -> ParseMap {
let mut m = ParseMap::new();
m.insert("objectId".into(), ParseValue::String(id.into()));
m.insert("username".into(), ParseValue::String(username.into()));
m
}
#[test]
fn the_exact_username_wins_over_a_matching_email_in_either_order() {
let target = row("collide@example.com", "TARGET");
let other = row("other_user", "OTHER");
for rows in [
vec![other.clone(), target.clone()],
vec![target.clone(), other.clone()],
] {
let picked = select_login_row(rows, Some("collide@example.com")).expect("a row");
assert!(
matches!(picked.get("objectId"), Some(ParseValue::String(id)) if id == "TARGET"),
"the username owner is chosen whichever row came first"
);
}
}
#[test]
fn a_multi_match_without_a_username_falls_back_to_the_first_row() {
let rows = vec![row("a", "FIRST"), row("b", "SECOND")];
let picked = select_login_row(rows, None).expect("a row");
assert!(matches!(picked.get("objectId"), Some(ParseValue::String(id)) if id == "FIRST"));
}
#[test]
fn email_validity_matches_javascripts_regex() {
for (email, expected) in [
("a@b", true),
("ab@cd", true),
("@a@b", true),
("a@b@", true),
("a b@c d", true),
("a\t@b", true),
("not-an-email", false),
("a@", false),
("@a", false),
("@", false),
("", false),
("a\n@b", false),
("a@\nb", false),
("a\r@b", false),
("a@\rb", false),
("a\u{2028}@b", false),
("a@\u{2029}b", false),
] {
assert_eq!(
is_valid_email(email),
expected,
"{email:?} ({:?})",
email.chars().map(|c| c as u32).collect::<Vec<_>>()
);
}
}
use super::*;
fn body(json: &str) -> WriteBody {
parse_rust_rest::decode_write_body(
&serde_json::from_str(json).expect("test literal"),
parse_rust_core::op::OpPath::Create,
)
.expect("decode")
}
#[tokio::test]
async fn shared_user_transform_removes_plaintext_and_writes_a_bcrypt_hash() {
let mut b = body(r#"{"password":"hunter2"}"#);
prepare_user_write(&mut b, false)
.await
.expect("password hashes");
assert!(!b.contains_key("password"));
let Some(FieldWrite::Value(ParseValue::String(hash))) = b.get(HASHED_PASSWORD) else {
panic!("hash missing");
};
assert!(parse_rust_auth::password::verify("hunter2".into(), hash.clone()).await);
}
#[test]
fn user_owner_acl_is_added_without_discarding_a_master_supplied_acl() {
let mut b = body(r#"{"objectId":"user123456","ACL":{"*":{"read":true}}}"#);
let object_id = ensure_user_identity_and_acl(&mut b).expect("string id");
assert_eq!(object_id, "user123456");
let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
panic!("ACL missing");
};
assert!(acl.contains_key("*"));
let Some(ParseValue::Object(owner)) = acl.get("user123456") else {
panic!("owner ACL missing");
};
assert!(matches!(owner.get("read"), Some(ParseValue::Bool(true))));
assert!(matches!(owner.get("write"), Some(ParseValue::Bool(true))));
}
#[test]
fn a_falsy_acl_on_signup_becomes_the_owner_acl_rather_than_a_public_row() {
for literal in [
r#"{"objectId":"user123456","ACL":null}"#,
r#"{"objectId":"user123456","ACL":false}"#,
r#"{"objectId":"user123456","ACL":0}"#,
r#"{"objectId":"user123456","ACL":""}"#,
] {
let mut b = body(literal);
ensure_user_identity_and_acl(&mut b).expect("string id");
let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
panic!("ACL missing or not an object for {literal}");
};
assert_eq!(acl.len(), 1, "{literal} produced {acl:?}");
let Some(ParseValue::Object(owner)) = acl.get("user123456") else {
panic!("owner entry missing for {literal}");
};
assert!(matches!(owner.get("read"), Some(ParseValue::Bool(true))));
assert!(matches!(owner.get("write"), Some(ParseValue::Bool(true))));
}
}
#[test]
fn a_js_object_acl_that_is_not_a_principal_map_still_gets_the_owner() {
for literal in [
r#"{"objectId":"user123456","ACL":{"__op":"Delete"}}"#,
r#"{"objectId":"user123456","ACL":{"__op":"Increment","amount":1}}"#,
r#"{"objectId":"user123456","ACL":[]}"#,
r#"{"objectId":"user123456","ACL":[1,2]}"#,
r#"{"objectId":"user123456","ACL":{"__type":"Date","iso":"2020-01-01T00:00:00.000Z"}}"#,
] {
let mut b = body(literal);
ensure_user_identity_and_acl(&mut b).expect("string id");
let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
panic!("ACL missing or not an object for {literal}");
};
assert_eq!(acl.len(), 1, "{literal} produced {acl:?}");
assert!(acl.contains_key("user123456"), "{literal}");
}
}
#[test]
fn a_truthy_scalar_acl_on_signup_is_left_for_the_validator() {
for literal in [
r#"{"objectId":"user123456","ACL":"nonsense"}"#,
r#"{"objectId":"user123456","ACL":123}"#,
r#"{"objectId":"user123456","ACL":true}"#,
] {
let mut b = body(literal);
ensure_user_identity_and_acl(&mut b).expect("string id");
assert!(
!matches!(b.get("ACL"), Some(FieldWrite::Value(ParseValue::Object(_)))),
"{literal} must be left alone"
);
}
}
#[test]
fn every_truthy_acl_shape_on_an_update_keeps_the_owner() {
for literal in [
r#"{"ACL":{"__op":"Increment","amount":1}}"#,
r#"{"ACL":{"__op":"Delete"}}"#,
r#"{"ACL":{"__op":"Add","objects":[1]}}"#,
r#"{"ACL":[]}"#,
r#"{"ACL":[1,2]}"#,
r#"{"ACL":{"__type":"Date","iso":"2020-01-01T00:00:00.000Z"}}"#,
r#"{"ACL":"nonsense"}"#,
r#"{"ACL":123}"#,
r#"{"ACL":true}"#,
] {
let mut b = body(literal);
force_owner_into_acl(&mut b, "user123456", false);
let Some(FieldWrite::Value(ParseValue::Object(acl))) = b.get("ACL") else {
panic!("ACL missing or not an object for {literal}");
};
let Some(ParseValue::Object(owner)) = acl.get("user123456") else {
panic!("owner entry missing for {literal}");
};
assert!(matches!(owner.get("read"), Some(ParseValue::Bool(true))));
assert!(matches!(owner.get("write"), Some(ParseValue::Bool(true))));
}
}
#[test]
fn a_falsy_acl_on_an_update_is_left_for_the_lowering_to_drop() {
for literal in [
r#"{"ACL":null}"#,
r#"{"ACL":false}"#,
r#"{"ACL":0}"#,
r#"{"ACL":""}"#,
] {
let mut b = body(literal);
force_owner_into_acl(&mut b, "user123456", false);
assert!(
!matches!(b.get("ACL"), Some(FieldWrite::Value(ParseValue::Object(_)))),
"{literal} must be left alone"
);
}
}
#[test]
fn a_privileged_caller_can_still_remove_the_owner() {
let mut b = body(r#"{"ACL":{"__op":"Delete"}}"#);
force_owner_into_acl(&mut b, "user123456", true);
assert!(matches!(b.get("ACL"), Some(FieldWrite::Op(_))));
}
#[test]
fn a_truthy_non_string_object_id_is_refused_rather_than_replaced() {
for literal in [
r#"{"objectId":123,"username":"u"}"#,
r#"{"objectId":true,"username":"u"}"#,
r#"{"objectId":["a"],"username":"u"}"#,
r#"{"objectId":{"a":1},"username":"u"}"#,
] {
let mut b = body(literal);
let e = ensure_user_identity_and_acl(&mut b).expect_err(literal);
assert_eq!(e.code, ErrorCode::IncorrectType, "{literal}");
}
}
#[test]
fn a_delete_operation_as_an_object_id_is_refused_with_invalid_json() {
let mut b = body(r#"{"objectId":{"__op":"Delete"},"username":"u"}"#);
let e = ensure_user_identity_and_acl(&mut b).expect_err("Delete op");
assert_eq!(e.code, ErrorCode::InvalidJson);
assert_eq!(e.message, "objectId is an invalid field name.");
}
#[test]
fn a_falsy_object_id_is_still_replaced_with_a_generated_one() {
for literal in [
r#"{"objectId":"","username":"u"}"#,
r#"{"objectId":null,"username":"u"}"#,
r#"{"username":"u"}"#,
] {
let mut b = body(literal);
let id = ensure_user_identity_and_acl(&mut b).expect(literal);
assert_eq!(id.len(), 10, "{literal} produced {id}");
}
}
#[tokio::test]
async fn an_update_is_only_hashed() {
let mut b = body(r#"{"password":"x","nickname":"n"}"#);
prepare_user_write(&mut b, false).await.expect("hash");
assert!(!b.contains_key("ACL"));
assert!(!b.contains_key("objectId"));
}
}