use std::future::Future;
use std::pin::Pin;
use indexmap::IndexMap;
use parse_rust_core::{
new_object_id, ErrorCode, FieldWrite, Op, Operation, ParseDate, ParseError, ParseMap,
ParseValue,
};
use parse_rust_schema::{
apply, default_schema, field_name_is_valid, infer::schema_mismatch, infer_op_type, infer_type,
validate_required_columns, validate_write_fields,
};
use parse_rust_storage::{
AddFieldOutcome, ClassSchema, Clause, Comparison, Constraint, FieldType, Query, QueryOptions,
SortDirection, StorageAdapter, UpdateValue, DEFAULT_LIMIT,
};
use crate::acl::{lower_acl, raise_acl, AclScope};
use crate::clp::{
adds_field, apply_pointer_permissions, deny_protected_fields, filter_sensitive_data,
plan_protected_fields, validate_permission, PermissionOptions, PointerPermOutcome,
ProtectedFieldPlan, WriteAction,
};
use crate::include;
use crate::query_parse::{ParsedClause, ParsedWhere};
use crate::relations::{self, RelationConstraint};
use crate::snapshot::SchemaSnapshot;
use crate::write::{
as_plain_body, echo_response, echoed_keys, flatten_for_create, lower_update, WriteBody,
};
type BoxFut<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub struct Ctx<'a, S: StorageAdapter> {
pub storage: &'a S,
pub snapshot: &'a SchemaSnapshot,
pub scope: &'a AclScope,
pub options: &'a PermissionOptions,
pub is_maintenance: bool,
}
impl<'a, S: StorageAdapter> Ctx<'a, S> {
pub fn new(
storage: &'a S,
snapshot: &'a SchemaSnapshot,
scope: &'a AclScope,
options: &'a PermissionOptions,
) -> Self {
Self {
is_maintenance: false,
storage,
snapshot,
scope,
options,
}
}
pub fn maintenance(mut self, yes: bool) -> Self {
self.is_maintenance = yes;
self
}
}
#[derive(Debug, Clone)]
pub struct FindOptions {
pub limit: Option<u32>,
pub skip: Option<u32>,
pub order: Vec<(String, SortDirection)>,
pub keys: Option<Vec<String>>,
pub exclude_keys: Option<Vec<String>>,
pub include: Vec<Vec<String>>,
}
impl Default for FindOptions {
fn default() -> Self {
Self {
limit: Some(DEFAULT_LIMIT),
skip: None,
order: Vec::new(),
keys: None,
exclude_keys: None,
include: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct CreateResponse {
pub object_id: String,
pub created_at: ParseDate,
pub echoed: ParseMap,
}
#[derive(Debug, Clone)]
pub struct UpdateResponse {
pub updated_at: ParseDate,
pub echoed: ParseMap,
}
fn object_not_found() -> ParseError {
ParseError::new(ErrorCode::ObjectNotFound, "Object not found.")
}
pub async fn find<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
where_: ParsedWhere,
options: FindOptions,
) -> Result<Vec<ParseMap>, ParseError> {
let op = derived_op(&where_);
let mut results = find_core(
ctx,
class_name,
where_,
options.clone(),
op,
ReadMethod::Find,
)
.await?;
expand_includes(ctx, &mut results, &options).await?;
Ok(results)
}
pub async fn get<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
object_id: &str,
options: FindOptions,
) -> Result<ParseMap, ParseError> {
let options = FindOptions {
limit: Some(1),
..options
};
let mut results = find_core(
ctx,
class_name,
pinned_where(object_id),
options.clone(),
Operation::Get,
ReadMethod::Get,
)
.await?;
expand_includes(ctx, &mut results, &options).await?;
results.into_iter().next().ok_or_else(object_not_found)
}
pub async fn count<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
where_: ParsedWhere,
) -> Result<u64, ParseError> {
let schema = ctx.snapshot.get_or_default(class_name);
let plan = plan_read(
ctx,
class_name,
&schema,
where_,
&[],
Operation::Count,
ReadMethod::Find,
)
.await?;
let query = match plan {
ReadPlan::Denied => return Ok(0),
ReadPlan::Run { query, .. } => query,
};
if !ctx.snapshot.contains(class_name) {
return Ok(0);
}
ctx.storage.count(&schema, &query).await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ReadMethod {
Get,
Find,
}
impl ReadMethod {
fn as_str(self) -> &'static str {
match self {
ReadMethod::Get => "get",
ReadMethod::Find => "find",
}
}
}
fn derived_op(where_: &ParsedWhere) -> Operation {
if where_.clauses.len() == 1 && where_.pinned_object_id().is_some() {
Operation::Get
} else {
Operation::Find
}
}
fn pinned_where(object_id: &str) -> ParsedWhere {
let mut where_ = ParsedWhere::default();
where_.push(ParsedClause::Field(Constraint::equal(
"objectId",
ParseValue::String(object_id.to_string()),
)));
where_
}
enum ReadPlan {
Run {
query: Query,
protected: Option<ProtectedFieldPlan>,
order: Vec<(String, SortDirection)>,
},
Denied,
}
pub const SESSION_CLASS: &str = "_Session";
pub const USER_CLASS: &str = "_User";
fn narrow_sessions(
where_: &mut ParsedWhere,
class_name: &str,
scope: &AclScope,
detail: parse_rust_core::ErrorDetail,
) -> Result<(), ParseError> {
if class_name != SESSION_CLASS || scope.is_master() {
return Ok(());
}
let Some(user_id) = scope.user_id() else {
return Err(ParseError::permission_denied(
ErrorCode::InvalidSessionToken,
"Invalid session token",
detail,
));
};
let mine = ParsedWhere {
clauses: vec![ParsedClause::Field(Constraint::equal(
"user",
ParseValue::Pointer {
class_name: USER_CLASS.to_string(),
object_id: user_id.to_string(),
},
))],
};
if where_.is_empty() {
*where_ = mine;
} else {
let client = std::mem::take(where_);
where_.push(ParsedClause::And(vec![client, mine]));
}
Ok(())
}
fn plan_read<'a, S: StorageAdapter>(
ctx: &'a Ctx<'a, S>,
class_name: &'a str,
schema: &'a ClassSchema,
mut where_: ParsedWhere,
order: &'a [(String, SortDirection)],
op: Operation,
method: ReadMethod,
) -> BoxFut<'a, Result<ReadPlan, ParseError>> {
Box::pin(async move {
let clp = ctx.snapshot.clp(class_name);
let acl_group = ctx.scope.acl_group();
let master = ctx.scope.is_master();
crate::class_security::enforce_class_security(
class_name,
master,
method.as_str(),
ctx.options.error_detail,
)?;
narrow_sessions(&mut where_, class_name, ctx.scope, ctx.options.error_detail)?;
let protected = if master {
None
} else {
plan_protected_fields(
class_name,
clp,
ctx.scope,
where_.pinned_object_id(),
ctx.options,
)
};
if !master {
deny_protected_fields(
protected.as_ref(),
class_name,
&where_,
order,
ctx.options.error_detail,
)?;
}
let order = validate_sort(schema, class_name, order)?;
if !master {
validate_permission(
clp,
class_name,
&acl_group,
op,
None,
ctx.options.error_detail,
)?;
}
crate::query_parse::validate_query_keys(&where_, master)?;
let mut query = resolve_where(ctx, class_name, schema, where_).await?;
if !master {
match apply_pointer_permissions(schema, clp, op, &acl_group, &query)? {
PointerPermOutcome::Unconstrained => {}
PointerPermOutcome::Constrained(narrowed) => query = narrowed,
PointerPermOutcome::DenyAll => return Ok(ReadPlan::Denied),
}
}
let constraint = match op {
Operation::Update | Operation::Delete => ctx.scope.write_constraint(),
_ => ctx.scope.read_constraint(),
};
if let Some(constraint) = constraint {
query.push_constraint(constraint);
}
Ok(ReadPlan::Run {
query,
protected,
order,
})
})
}
fn find_core<'a, S: StorageAdapter>(
ctx: &'a Ctx<'a, S>,
class_name: &'a str,
where_: ParsedWhere,
options: FindOptions,
op: Operation,
method: ReadMethod,
) -> BoxFut<'a, Result<Vec<ParseMap>, ParseError>> {
Box::pin(async move {
let schema = ctx.snapshot.get_or_default(class_name);
let plan = plan_read(ctx, class_name, &schema, where_, &options.order, op, method).await?;
let (query, protected, order) = match plan {
ReadPlan::Denied => {
return if op == Operation::Get {
Err(object_not_found())
} else {
Ok(Vec::new())
};
}
ReadPlan::Run {
query,
protected,
order,
} => (query, protected, order),
};
if !ctx.snapshot.contains(class_name) {
validate_client_class_creation(ctx, class_name, false)?;
return Ok(Vec::new());
}
let query_options = QueryOptions {
limit: options.limit,
skip: options.skip,
order,
keys: projection(&schema, &options),
case_insensitive: false,
};
let rows = ctx.storage.find(&schema, &query, &query_options).await?;
let is_read = matches!(op, Operation::Get | Operation::Find);
Ok(rows
.into_iter()
.map(|row| {
let mut row = raise_acl(row);
filter_sensitive_data(
&mut row,
class_name,
ctx.scope,
protected.as_ref(),
is_read,
ctx.options,
);
row
})
.collect())
})
}
fn projection(schema: &ClassSchema, options: &FindOptions) -> Option<Vec<String>> {
const ALWAYS: [&str; 4] = ["objectId", "createdAt", "updatedAt", "ACL"];
match (&options.keys, &options.exclude_keys) {
(None, None) => None,
(Some(keys), None) => {
let mut out = keys.clone();
for key in ALWAYS {
if !out.iter().any(|k| k == key) {
out.push(key.to_string());
}
}
Some(out)
}
(keys, Some(exclude)) => {
let exclude: Vec<&String> = exclude
.iter()
.filter(|k| !ALWAYS.contains(&k.as_str()))
.collect();
let base: Vec<String> = match keys {
Some(keys) => {
let mut out = keys.clone();
for key in ALWAYS {
if !out.iter().any(|k| k == key) {
out.push(key.to_string());
}
}
out
}
None => schema.fields.keys().cloned().collect(),
};
Some(base.into_iter().filter(|k| !exclude.contains(&k)).collect())
}
}
}
fn resolve_where<'a, S: StorageAdapter>(
ctx: &'a Ctx<'a, S>,
class_name: &'a str,
schema: &'a ClassSchema,
where_: ParsedWhere,
) -> BoxFut<'a, Result<Query, ParseError>> {
Box::pin(async move {
let mut query = Query::new();
let mut relation_groups: IndexMap<String, Vec<Comparison>> = IndexMap::new();
for clause in where_.clauses {
match clause {
ParsedClause::Field(constraint) => {
match schema.field(&constraint.field) {
Some(FieldType::Relation { .. }) => {
relation_groups
.entry(constraint.field.clone())
.or_default()
.push(constraint.comparison);
}
_ => query.push_constraint(constraint),
}
}
ParsedClause::RelatedTo {
class_name: owning_class,
object_id: owning_id,
key,
} => {
let outcome = resolve_related_to(ctx, &owning_class, &owning_id, &key).await?;
relations::add_in_object_ids(&mut query, outcome.ids());
}
ParsedClause::Or(branches) => {
query.push(Clause::Or(
resolve_branches(ctx, class_name, schema, branches).await?,
));
}
ParsedClause::And(branches) => {
query.push(Clause::And(
resolve_branches(ctx, class_name, schema, branches).await?,
));
}
ParsedClause::Nor(branches) => {
query.push(Clause::Nor(
resolve_branches(ctx, class_name, schema, branches).await?,
));
}
}
}
for (field, comparisons) in relation_groups {
for constraint in relations::relation_constraints_for(&comparisons)? {
match constraint {
RelationConstraint::OwnersOf(ids) => {
let owners =
relations::owning_ids(ctx.storage, class_name, &field, &ids).await?;
relations::add_in_object_ids(&mut query, &owners);
}
RelationConstraint::NotOwnersOf(ids) => {
let owners =
relations::owning_ids(ctx.storage, class_name, &field, &ids).await?;
relations::add_not_in_object_ids(&mut query, &owners);
}
}
}
}
Ok(query)
})
}
async fn resolve_branches<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
schema: &ClassSchema,
branches: Vec<ParsedWhere>,
) -> Result<Vec<Query>, ParseError> {
let mut out = Vec::with_capacity(branches.len());
for branch in branches {
out.push(resolve_where(ctx, class_name, schema, branch).await?);
}
Ok(out)
}
async fn resolve_related_to<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
owning_class: &str,
owning_id: &str,
key: &str,
) -> Result<relations::RelatedToOutcome, ParseError> {
if ctx.scope.is_master() {
let ids = relations::related_ids(ctx.storage, owning_class, key, owning_id).await?;
return Ok(relations::RelatedToOutcome::Ids(ids));
}
let owning_protected = plan_protected_fields(
owning_class,
ctx.snapshot.clp(owning_class),
ctx.scope,
Some(owning_id),
ctx.options,
)
.map(|p| p.strip)
.unwrap_or_default();
let authorized = relations::authorize_related_to(
owning_class,
key,
&owning_protected,
ctx.options.error_detail,
|| async move {
let options = FindOptions {
limit: Some(1),
keys: Some(vec!["objectId".to_string()]),
..Default::default()
};
match find_core(
ctx,
owning_class,
pinned_where(owning_id),
options,
Operation::Get,
ReadMethod::Get,
)
.await
{
Ok(rows) => Ok(!rows.is_empty()),
Err(e)
if e.code == ErrorCode::OperationForbidden
|| e.code == ErrorCode::ObjectNotFound =>
{
Ok(false)
}
Err(e) => Err(e),
}
},
)
.await?;
if !authorized {
return Ok(relations::RelatedToOutcome::DeniedYieldEmpty);
}
let ids = relations::related_ids(ctx.storage, owning_class, key, owning_id).await?;
Ok(relations::RelatedToOutcome::Ids(ids))
}
fn validate_sort(
schema: &ClassSchema,
class_name: &str,
order: &[(String, SortDirection)],
) -> Result<Vec<(String, SortDirection)>, ParseError> {
let mut out = Vec::new();
for (field, direction) in order {
if is_auth_data_id_path(field) {
return Err(ParseError::invalid_key_name(format!(
"Cannot sort by {field}"
)));
}
let root = field.split('.').next().unwrap_or(field);
if !field_name_is_valid(root, class_name) {
return Err(ParseError::invalid_key_name(format!(
"Invalid field name: {field}."
)));
}
if schema.field(root).is_none() && field != "score" {
continue;
}
out.push((field.clone(), *direction));
}
Ok(out)
}
fn is_auth_data_id_path(field: &str) -> bool {
let parts: Vec<&str> = field.split('.').collect();
matches!(parts.as_slice(), ["authData", provider, "id"]
if !provider.is_empty()
&& provider.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
}
async fn expand_includes<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
results: &mut [ParseMap],
options: &FindOptions,
) -> Result<(), ParseError> {
if options.include.is_empty() || results.is_empty() {
return Ok(());
}
let keys = options.keys.clone().unwrap_or_default();
let exclude_keys = options.exclude_keys.clone().unwrap_or_default();
for path in &options.include {
let by_class = include::collect_pointers(results, path);
if by_class.is_empty() {
continue;
}
let mut fetched: IndexMap<String, ParseMap> = IndexMap::new();
for (target_class, ids) in by_class.iter() {
let mut where_ = ParsedWhere::default();
let method = if ids.len() == 1 {
ReadMethod::Get
} else {
ReadMethod::Find
};
let constraint = if ids.len() == 1 {
Constraint::equal("objectId", ParseValue::String(ids[0].clone()))
} else {
Constraint::one_of(
"objectId",
ids.iter()
.map(|id| ParseValue::String(id.clone()))
.collect(),
)
};
where_.push(ParsedClause::Field(constraint));
let nested = FindOptions {
limit: Some(ids.len() as u32),
skip: None,
order: Vec::new(),
keys: include::keys_for_path(&keys, path),
exclude_keys: include::exclude_keys_for_path(&exclude_keys, path),
include: Vec::new(),
};
let rows = find_core(ctx, target_class, where_, nested, Operation::Get, method).await?;
for mut row in rows {
let Some(ParseValue::String(id)) = row.get("objectId").cloned() else {
continue;
};
include::shape_included(&mut row, target_class, ctx.scope.is_master());
fetched.insert(id, row);
}
}
include::graft(results, path, &fetched);
}
Ok(())
}
pub async fn create<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
mut body: WriteBody,
) -> Result<CreateResponse, ParseError> {
let class_exists = ctx.snapshot.contains(class_name);
let mut schema = ctx.snapshot.resolve_for_write(class_name);
let clp = ctx.snapshot.clp(class_name);
let acl_group = ctx.scope.acl_group();
let master = ctx.scope.is_master();
validate_required_columns(class_name, &as_plain_body(&body), false)?;
if !master
&& adds_field(
&schema,
class_exists,
body.keys().map(String::as_str),
|key| matches!(body.get(key), Some(FieldWrite::Op(Op::Delete))),
)
{
validate_permission(
clp,
class_name,
&acl_group,
Operation::AddField,
Some(WriteAction::Create),
ctx.options.error_detail,
)?;
}
if !master {
validate_permission(
clp,
class_name,
&acl_group,
Operation::Create,
Some(WriteAction::Create),
ctx.options.error_detail,
)?;
}
ensure_class_exists(ctx, class_name, class_exists).await?;
let object_id = match body.get("objectId") {
None => new_object_id(),
Some(FieldWrite::Value(v)) if !parse_rust_core::is_js_truthy(v) => new_object_id(),
Some(FieldWrite::Value(ParseValue::String(id))) => id.clone(),
Some(other) => {
let got = match other {
FieldWrite::Value(v) => infer_type(v),
FieldWrite::Op(op) => infer_op_type(op)?,
};
return Err(match got {
Some(got) => schema_mismatch(class_name, "objectId", &FieldType::String, &got),
None => ParseError::invalid_json("objectId is an invalid field name."),
});
}
};
let now = ParseDate::now();
body.insert(
"objectId".to_string(),
FieldWrite::Value(ParseValue::String(object_id.clone())),
);
body.insert(
"createdAt".to_string(),
FieldWrite::Value(ParseValue::Date(now)),
);
body.insert(
"updatedAt".to_string(),
FieldWrite::Value(ParseValue::Date(now)),
);
let delta = validate_write_fields(&schema, &body)?;
reserve_schema(ctx, class_name, &schema, &delta.added).await?;
apply(&mut schema, &delta);
let relation_updates = relations::collect_relation_updates(&mut body);
let row = lower_acl(flatten_for_create(&body)?);
ctx.storage.create(&schema, &row).await?;
relations::apply_relation_updates(ctx.storage, class_name, &object_id, &relation_updates)
.await?;
Ok(CreateResponse {
object_id,
created_at: now,
echoed: echo_response(&body, Some(&row)),
})
}
pub async fn update<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
object_id: &str,
mut body: WriteBody,
) -> Result<UpdateResponse, ParseError> {
let class_exists = ctx.snapshot.contains(class_name);
let mut schema = ctx.snapshot.resolve_for_write(class_name);
let clp = ctx.snapshot.clp(class_name);
let acl_group = ctx.scope.acl_group();
let master = ctx.scope.is_master();
body.shift_remove("objectId");
body.shift_remove("createdAt");
validate_required_columns(class_name, &as_plain_body(&body), true)?;
let introduces_field = adds_field(
&schema,
class_exists,
body.keys().map(String::as_str),
|key| matches!(body.get(key), Some(FieldWrite::Op(Op::Delete))),
);
if !master && introduces_field {
validate_permission(
clp,
class_name,
&acl_group,
Operation::AddField,
Some(WriteAction::Update),
ctx.options.error_detail,
)?;
}
if !master {
validate_permission(
clp,
class_name,
&acl_group,
Operation::Update,
Some(WriteAction::Update),
ctx.options.error_detail,
)?;
}
let mut query = Query::from_constraints(vec![Constraint::equal(
"objectId",
ParseValue::String(object_id.to_string()),
)]);
if !master {
match apply_pointer_permissions(&schema, clp, Operation::Update, &acl_group, &query)? {
PointerPermOutcome::Unconstrained => {}
PointerPermOutcome::Constrained(narrowed) => query = narrowed,
PointerPermOutcome::DenyAll => return Err(object_not_found()),
}
if introduces_field {
match apply_pointer_permissions(&schema, clp, Operation::AddField, &acl_group, &query)?
{
PointerPermOutcome::Unconstrained => {}
PointerPermOutcome::Constrained(narrowed) => query = narrowed,
PointerPermOutcome::DenyAll => return Err(object_not_found()),
}
}
if let Some(constraint) = ctx.scope.write_constraint() {
query.push_constraint(constraint);
}
}
let updated_at = ParseDate::now();
body.insert(
"updatedAt".to_string(),
FieldWrite::Value(ParseValue::Date(updated_at)),
);
ensure_class_exists(ctx, class_name, class_exists).await?;
let delta = validate_write_fields(&schema, &body)?;
reserve_schema(ctx, class_name, &schema, &delta.added).await?;
apply(&mut schema, &delta);
let relation_updates = relations::collect_relation_updates(&mut body);
let mut update = lower_update(&body)?;
lower_acl_into_update(&mut body, &mut update);
let echoed = if echoed_keys(&body).is_empty() {
let matched = ctx.storage.update(&schema, &query, &update).await?;
if matched == 0 {
return Err(object_not_found());
}
ParseMap::new()
} else {
let row = ctx
.storage
.update_one_returning(&schema, &query, &update)
.await?;
let Some(row) = row else {
return Err(object_not_found());
};
echo_response(&body, Some(&row))
};
relations::apply_relation_updates(ctx.storage, class_name, object_id, &relation_updates)
.await?;
Ok(UpdateResponse { updated_at, echoed })
}
fn lower_acl_into_update(body: &mut WriteBody, update: &mut parse_rust_storage::Update) {
let Some(write) = body.shift_remove("ACL") else {
return;
};
update.shift_remove("ACL");
let value = match write {
FieldWrite::Value(v) if !parse_rust_core::is_js_truthy(&v) => return,
FieldWrite::Value(value) => value,
FieldWrite::Op(_) => ParseValue::Object(ParseMap::new()),
};
let mut carrier = ParseMap::new();
carrier.insert("ACL".to_string(), value);
let lowered = lower_acl(carrier);
for key in ["_rperm", "_wperm"] {
let value = lowered
.get(key)
.cloned()
.unwrap_or(ParseValue::Array(Vec::new()));
update.insert(key.to_string(), UpdateValue::Set(value));
}
}
pub async fn delete<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
object_id: &str,
) -> Result<(), ParseError> {
let schema = ctx.snapshot.get_or_default(class_name);
let clp = ctx.snapshot.clp(class_name);
let acl_group = ctx.scope.acl_group();
let master = ctx.scope.is_master();
if !master {
validate_permission(
clp,
class_name,
&acl_group,
Operation::Delete,
None,
ctx.options.error_detail,
)?;
}
let mut query = Query::from_constraints(vec![Constraint::equal(
"objectId",
ParseValue::String(object_id.to_string()),
)]);
if !master {
match apply_pointer_permissions(&schema, clp, Operation::Delete, &acl_group, &query)? {
PointerPermOutcome::Unconstrained => {}
PointerPermOutcome::Constrained(narrowed) => query = narrowed,
PointerPermOutcome::DenyAll => return Err(object_not_found()),
}
if let Some(constraint) = ctx.scope.write_constraint() {
query.push_constraint(constraint);
}
}
let deleted = ctx.storage.delete(&schema, &query).await?;
if deleted == 0 {
return Err(object_not_found());
}
Ok(())
}
async fn ensure_class_exists<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
class_exists: bool,
) -> Result<(), ParseError> {
if class_exists {
return Ok(());
}
validate_client_class_creation(ctx, class_name, true)?;
if !parse_rust_schema::class_name_is_valid(class_name) {
return Err(ParseError::invalid_json(
"schema class name does not revalidate",
));
}
ctx.storage.upsert_schema(&default_schema(class_name)).await
}
fn validate_client_class_creation<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
maintenance_is_exempt: bool,
) -> Result<(), ParseError> {
let privileged = if maintenance_is_exempt {
ctx.scope.is_master()
} else {
ctx.scope.is_master() && !ctx.is_maintenance
};
if ctx.options.allow_client_class_creation
|| privileged
|| parse_rust_schema::SYSTEM_CLASSES.contains(&class_name)
{
return Ok(());
}
Err(ParseError::permission_denied(
ErrorCode::OperationForbidden,
format!("This user is not allowed to access non-existent class: {class_name}"),
ctx.options.error_detail,
))
}
async fn reserve_schema<S: StorageAdapter>(
ctx: &Ctx<'_, S>,
class_name: &str,
schema: &ClassSchema,
added: &[(String, FieldType)],
) -> Result<(), ParseError> {
for (field_name, field_type) in added {
match ctx
.storage
.reserve_field(class_name, field_name, field_type, None)
.await?
{
AddFieldOutcome::Added | AddFieldOutcome::AlreadyPresentSameType => {}
AddFieldOutcome::Conflict { existing } => {
return Err(schema_mismatch(
&schema.class_name,
field_name,
&existing,
field_type,
));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query_parse::parse_where;
use crate::testing::FakeStorage;
use crate::write::decode_write_body;
use parse_rust_core::op::OpPath;
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 where_(json: &str) -> ParsedWhere {
parse_where(&serde_json::from_str(json).expect("test literal")).expect("parse")
}
fn body(json: &str, path: OpPath) -> WriteBody {
decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
}
fn row(pairs: Vec<(&str, ParseValue)>) -> ParseMap {
let mut m = ParseMap::new();
for (k, v) in pairs {
m.insert(k.to_string(), v);
}
m
}
fn strings(values: &[&str]) -> ParseValue {
ParseValue::Array(
values
.iter()
.map(|v| ParseValue::String((*v).to_string()))
.collect(),
)
}
fn pointer(class: &str, id: &str) -> ParseValue {
ParseValue::Pointer {
class_name: class.to_string(),
object_id: id.to_string(),
}
}
async fn snapshot(storage: &FakeStorage) -> SchemaSnapshot {
SchemaSnapshot::load(storage).await.expect("snapshot")
}
fn opts() -> PermissionOptions {
PermissionOptions::default()
}
fn disclosing_opts() -> PermissionOptions {
PermissionOptions {
error_detail: parse_rust_core::ErrorDetail::Disclosed,
..PermissionOptions::default()
}
}
#[tokio::test]
async fn create_runs_the_clp_gate() {
let storage = FakeStorage::new().with_schema(
default_schema("Post").with_clp(clp(r#"{"create":{"role:Writers":true}}"#)),
);
let snap = snapshot(&storage).await;
let options = opts();
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
let e = create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
.await
.unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
assert_eq!(e.message, "Permission denied");
assert!(storage.rows("Post").is_empty(), "nothing was written");
let disclosing = disclosing_opts();
let ctx = Ctx::new(&storage, &snap, &anon, &disclosing);
assert_eq!(
create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
.await
.unwrap_err()
.message,
"Permission denied for action create on class Post."
);
assert!(storage.rows("Post").is_empty(), "nothing was written");
let writer = AclScope::user("u1", vec!["Writers".into()]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &writer, &options);
assert!(
create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
.await
.is_ok()
);
}
#[tokio::test]
async fn a_class_with_no_clp_is_unrestricted() {
let storage = FakeStorage::new().with_schema(default_schema("Post"));
let snap = snapshot(&storage).await;
let options = opts();
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
assert!(
create(&ctx, "Post", body(r#"{"title":"x"}"#, OpPath::Create))
.await
.is_ok()
);
assert!(
find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.is_ok()
);
}
#[tokio::test]
async fn requires_authentication_denies_a_read_with_object_not_found() {
let storage = FakeStorage::new()
.with_schema(
default_schema("Post").with_clp(clp(r#"{"find":{"requiresAuthentication":true}}"#)),
)
.with_row(
"Post",
row(vec![("objectId", ParseValue::String("p1".into()))]),
);
let snap = snapshot(&storage).await;
let options = opts();
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
let e = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.unwrap_err();
assert_eq!(e.code, ErrorCode::ObjectNotFound);
assert_eq!(e.message, "Permission denied");
let disclosing = disclosing_opts();
let disclosed = Ctx::new(&storage, &snap, &anon, &disclosing);
let e = find(
&disclosed,
"Post",
ParsedWhere::default(),
FindOptions::default(),
)
.await
.unwrap_err();
assert_eq!(e.code, ErrorCode::ObjectNotFound);
assert_eq!(
e.message,
"Permission denied, user needs to be authenticated."
);
let user = AclScope::user("u1", vec![]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &user, &options);
assert_eq!(
find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.expect("allowed")
.len(),
1
);
}
fn pointer_perm_storage() -> FakeStorage {
let clp_json = r#"{
"find":{"pointerFields":["owner"]},
"get":{"pointerFields":["owner"]},
"count":{"pointerFields":["owner"]},
"update":{"pointerFields":["owner"]},
"delete":{"pointerFields":["owner"]},
"create":{"*":true}
}"#;
FakeStorage::new()
.with_schema(
default_schema("Post")
.with_field(
"owner",
FieldType::Pointer {
target_class: "_User".into(),
},
)
.with_field("title", FieldType::String)
.with_clp(clp(clp_json)),
)
.with_row(
"Post",
row(vec![
("objectId", ParseValue::String("p1".into())),
("owner", pointer("_User", "u1")),
("title", ParseValue::String("mine".into())),
]),
)
.with_row(
"Post",
row(vec![
("objectId", ParseValue::String("p2".into())),
("owner", pointer("_User", "u2")),
("title", ParseValue::String("theirs".into())),
]),
)
}
#[tokio::test]
async fn an_anonymous_caller_gets_nothing_from_a_pointer_permission_class() {
let storage = pointer_perm_storage();
let snap = snapshot(&storage).await;
let options = opts();
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
assert!(
find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.expect("find resolves empty rather than erroring")
.is_empty()
);
assert_eq!(
get(&ctx, "Post", "p1", FindOptions::default())
.await
.unwrap_err()
.code,
ErrorCode::ObjectNotFound
);
assert_eq!(
count(&ctx, "Post", ParsedWhere::default())
.await
.expect("count resolves"),
0
);
assert_eq!(
update(&ctx, "Post", "p1", body(r#"{"title":"x"}"#, OpPath::Update))
.await
.unwrap_err()
.code,
ErrorCode::ObjectNotFound
);
assert_eq!(
delete(&ctx, "Post", "p1").await.unwrap_err().code,
ErrorCode::ObjectNotFound
);
assert_eq!(storage.rows("Post").len(), 2, "nothing was deleted");
}
#[tokio::test]
async fn a_pointer_permission_narrows_a_user_to_their_own_rows() {
let storage = pointer_perm_storage();
let snap = snapshot(&storage).await;
let options = opts();
let u1 = AclScope::user("u1", vec![]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &u1, &options);
let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.expect("find");
assert_eq!(results.len(), 1);
assert!(matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "p1"));
assert!(get(&ctx, "Post", "p1", FindOptions::default())
.await
.is_ok());
assert_eq!(
get(&ctx, "Post", "p2", FindOptions::default())
.await
.unwrap_err()
.code,
ErrorCode::ObjectNotFound
);
assert_eq!(
count(&ctx, "Post", ParsedWhere::default())
.await
.expect("count"),
1
);
assert!(
update(&ctx, "Post", "p1", body(r#"{"title":"x"}"#, OpPath::Update))
.await
.is_ok()
);
assert_eq!(
update(&ctx, "Post", "p2", body(r#"{"title":"x"}"#, OpPath::Update))
.await
.unwrap_err()
.code,
ErrorCode::ObjectNotFound
);
}
fn protected_storage() -> FakeStorage {
FakeStorage::new()
.with_schema(
default_schema("Post")
.with_field("secret", FieldType::String)
.with_field("title", FieldType::String)
.with_clp(clp(r#"{"protectedFields":{"*":["secret"]}}"#)),
)
.with_row(
"Post",
row(vec![
("objectId", ParseValue::String("p1".into())),
("title", ParseValue::String("t".into())),
("secret", ParseValue::String("s".into())),
]),
)
}
#[tokio::test]
async fn a_protected_field_is_absent_from_a_read_and_present_for_master() {
let storage = protected_storage();
let snap = snapshot(&storage).await;
let options = opts();
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.expect("find");
assert!(results[0].get("secret").is_none());
assert!(results[0].get("title").is_some());
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.expect("find");
assert!(results[0].get("secret").is_some());
}
#[tokio::test]
async fn querying_or_ordering_by_a_protected_field_is_forbidden() {
let storage = protected_storage();
let snap = snapshot(&storage).await;
let options = opts();
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
let e = find(
&ctx,
"Post",
where_(r#"{"secret":"s"}"#),
FindOptions::default(),
)
.await
.unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
assert_eq!(e.message, "Permission denied");
let disclosing = disclosing_opts();
let disclosed = Ctx::new(&storage, &snap, &anon, &disclosing);
assert_eq!(
find(
&disclosed,
"Post",
where_(r#"{"secret":"s"}"#),
FindOptions::default()
)
.await
.unwrap_err()
.message,
"This user is not allowed to query secret on class Post"
);
assert_eq!(
find(
&ctx,
"Post",
where_(r#"{"$or":[{"secret.a":"s"}]}"#),
FindOptions::default()
)
.await
.unwrap_err()
.code,
ErrorCode::OperationForbidden
);
let sorted = FindOptions {
order: vec![("secret".to_string(), SortDirection::Ascending)],
..Default::default()
};
let e = find(&ctx, "Post", ParsedWhere::default(), sorted.clone())
.await
.unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
assert_eq!(e.message, "Permission denied");
assert_eq!(
find(&disclosed, "Post", ParsedWhere::default(), sorted)
.await
.unwrap_err()
.message,
"This user is not allowed to sort by secret on class Post"
);
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
assert!(find(
&ctx,
"Post",
where_(r#"{"secret":"s"}"#),
FindOptions::default()
)
.await
.is_ok());
}
#[tokio::test]
async fn an_acl_hides_a_row_from_everyone_but_its_principals() {
let storage = FakeStorage::new()
.with_schema(default_schema("Post"))
.with_row(
"Post",
row(vec![
("objectId", ParseValue::String("private".into())),
("_rperm", strings(&["u1", "role:Admins"])),
("_wperm", strings(&["u1"])),
]),
)
.with_row(
"Post",
row(vec![("objectId", ParseValue::String("public".into()))]),
);
let snap = snapshot(&storage).await;
let options = opts();
let owner = AclScope::user("u1", vec![]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &owner, &options);
assert_eq!(
find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.expect("find")
.len(),
2,
"assert first that the owner can see its own row"
);
let admin = AclScope::user("u2", vec!["Admins".into()]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &admin, &options);
assert_eq!(
find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.expect("find")
.len(),
2,
"a role: entry in _rperm must match a member"
);
let stranger = AclScope::user("u3", vec![]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &stranger, &options);
let results = find(&ctx, "Post", ParsedWhere::default(), FindOptions::default())
.await
.expect("find");
assert_eq!(results.len(), 1);
assert!(
matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "public")
);
assert_eq!(
update(
&ctx,
"Post",
"private",
body(r#"{"title":"x"}"#, OpPath::Update)
)
.await
.unwrap_err()
.code,
ErrorCode::ObjectNotFound
);
assert_eq!(
delete(&ctx, "Post", "private").await.unwrap_err().code,
ErrorCode::ObjectNotFound
);
}
#[tokio::test]
async fn operations_reach_storage_as_operations() {
let storage = FakeStorage::new();
let snap = snapshot(&storage).await;
let options = opts();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let created = create(
&ctx,
"Post",
body(
r#"{"views":{"__op":"Increment","amount":2},"tags":{"__op":"Add","objects":["a"]}}"#,
OpPath::Create,
),
)
.await
.expect("create");
let stored = storage.rows("Post");
assert!(
matches!(stored[0].get("views"), Some(ParseValue::Number(n)) if *n == 2.0),
"an Increment must be flattened to a number, not stored as an op envelope"
);
assert!(matches!(stored[0].get("tags"), Some(ParseValue::Array(a)) if a.len() == 1));
assert!(matches!(created.echoed.get("views"), Some(ParseValue::Number(n)) if *n == 2.0));
let snap = snapshot(&storage).await;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let updated = update(
&ctx,
"Post",
&created.object_id,
body(
r#"{"views":{"__op":"Increment","amount":3},"title":"plain"}"#,
OpPath::Update,
),
)
.await
.expect("update");
assert!(
matches!(updated.echoed.get("views"), Some(ParseValue::Number(n)) if *n == 5.0),
"the response carries the post-update value"
);
assert!(
updated.echoed.get("title").is_none(),
"a plain set is not echoed"
);
assert!(
matches!(storage.rows("Post")[0].get("views"), Some(ParseValue::Number(n)) if *n == 5.0)
);
}
#[tokio::test]
async fn a_delete_op_unsets_the_field_and_echoes_nothing() {
let storage = FakeStorage::new()
.with_schema(default_schema("Post").with_field("title", FieldType::String))
.with_row(
"Post",
row(vec![
("objectId", ParseValue::String("p1".into())),
("title", ParseValue::String("t".into())),
]),
);
let snap = snapshot(&storage).await;
let options = opts();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let response = update(
&ctx,
"Post",
"p1",
body(r#"{"title":{"__op":"Delete"}}"#, OpPath::Update),
)
.await
.expect("update");
assert!(response.echoed.is_empty());
assert!(storage.rows("Post")[0].get("title").is_none());
}
#[tokio::test]
async fn a_type_conflict_fails_the_write_before_the_row_is_inserted() {
let storage = FakeStorage::new()
.with_schema(default_schema("Post").with_field("views", FieldType::Number));
let snap = SchemaSnapshot::from_classes(vec![default_schema("Post")]);
let options = opts();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let e = create(&ctx, "Post", body(r#"{"views":"text"}"#, OpPath::Create))
.await
.unwrap_err();
assert_eq!(e.code, ErrorCode::IncorrectType);
assert_eq!(
e.message,
"schema mismatch for Post.views; expected Number but got String"
);
assert!(storage.rows("Post").is_empty(), "no row was inserted");
}
#[tokio::test]
async fn a_write_of_only_nulls_still_creates_the_class() {
let storage = FakeStorage::new();
let snap = snapshot(&storage).await;
let options = opts();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
create(&ctx, "Post", body(r#"{"nothing":null}"#, OpPath::Create))
.await
.expect("create");
let schema = storage
.schema("Post")
.expect("a null-only write must still leave a schema row behind");
assert!(schema.field("objectId").is_some());
assert!(schema.field("nothing").is_none(), "null infers no type");
}
#[tokio::test]
async fn a_relation_write_lands_in_the_join_table_and_reads_back_through_related_to() {
let storage = FakeStorage::new()
.with_schema(default_schema("_Role"))
.with_schema(default_schema("_User"))
.with_row(
"_User",
row(vec![("objectId", ParseValue::String("u1".into()))]),
);
let snap = snapshot(&storage).await;
let options = opts();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let created = create(
&ctx,
"_Role",
body(
r#"{"name":"admins","ACL":{"*":{"read":true}},
"users":{"__op":"AddRelation","objects":[
{"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
OpPath::Create,
),
)
.await
.expect("create");
let stored = storage.rows("_Role");
assert!(
stored[0].get("users").is_none(),
"a Relation field has no column"
);
let joins = storage.rows("_Join:users:_Role");
assert_eq!(joins.len(), 1);
assert!(matches!(joins[0].get("relatedId"), Some(ParseValue::String(id)) if id == "u1"));
assert!(
matches!(joins[0].get("owningId"), Some(ParseValue::String(id)) if *id == created.object_id)
);
assert!(
storage.schema("_Join:users:_Role").is_none(),
"a join collection has no _SCHEMA row"
);
let snap = snapshot(&storage).await;
let ctx = Ctx::new(&storage, &snap, &master, &options);
update(
&ctx,
"_Role",
&created.object_id,
body(
r#"{"users":{"__op":"AddRelation","objects":[
{"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
OpPath::Update,
),
)
.await
.expect("update");
assert_eq!(storage.rows("_Join:users:_Role").len(), 1);
let query = format!(
r#"{{"$relatedTo":{{"object":{{"__type":"Pointer","className":"_Role","objectId":"{}"}},"key":"users"}}}}"#,
created.object_id
);
let members = find(&ctx, "_User", where_(&query), FindOptions::default())
.await
.expect("find");
assert_eq!(members.len(), 1);
update(
&ctx,
"_Role",
&created.object_id,
body(
r#"{"users":{"__op":"RemoveRelation","objects":[
{"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
OpPath::Update,
),
)
.await
.expect("update");
assert!(storage.rows("_Join:users:_Role").is_empty());
}
#[tokio::test]
async fn a_related_to_the_caller_cannot_read_yields_empty_rather_than_an_error() {
let storage = FakeStorage::new()
.with_schema(default_schema("_Role"))
.with_schema(default_schema("_User"))
.with_row(
"_Role",
row(vec![
("objectId", ParseValue::String("r1".into())),
("_rperm", strings(&["u2"])),
]),
)
.with_row(
"_User",
row(vec![("objectId", ParseValue::String("u1".into()))]),
)
.with_row(
"_Join:users:_Role",
row(vec![
("relatedId", ParseValue::String("u1".into())),
("owningId", ParseValue::String("r1".into())),
]),
);
let snap = snapshot(&storage).await;
let options = opts();
let query = r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#;
let outsider = AclScope::user("u3", vec![]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &outsider, &options);
let results = find(&ctx, "_User", where_(query), FindOptions::default())
.await
.expect("a denied relation is empty, not an error");
assert!(results.is_empty());
let insider = AclScope::user("u2", vec![]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &insider, &options);
assert_eq!(
find(&ctx, "_User", where_(query), FindOptions::default())
.await
.expect("find")
.len(),
1
);
}
#[tokio::test]
async fn a_related_to_on_a_protected_key_is_forbidden() {
let storage = FakeStorage::new()
.with_schema(default_schema("_User"))
.with_schema(
default_schema("_Role")
.with_field(
"users",
FieldType::Relation {
target_class: "_User".into(),
},
)
.with_clp(clp(r#"{"protectedFields":{"*":["users"]}}"#)),
)
.with_row(
"_Role",
row(vec![("objectId", ParseValue::String("r1".into()))]),
);
let snap = snapshot(&storage).await;
let options = opts();
let user = AclScope::user("u1", vec![]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &user, &options);
let query = r#"{"$relatedTo":{"object":{"__type":"Pointer","className":"_Role","objectId":"r1"},"key":"users"}}"#;
let e = find(&ctx, "_User", where_(query), FindOptions::default())
.await
.unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
assert_eq!(e.message, "Permission denied");
let disclosing = disclosing_opts();
let disclosed = Ctx::new(&storage, &snap, &user, &disclosing);
assert_eq!(
find(&disclosed, "_User", where_(query), FindOptions::default())
.await
.unwrap_err()
.message,
"This user is not allowed to query users on class _Role"
);
}
#[tokio::test]
async fn a_constraint_on_a_relation_field_is_the_reverse_join() {
let storage = FakeStorage::new()
.with_schema(default_schema("_User"))
.with_schema(default_schema("_Role").with_field(
"users",
FieldType::Relation {
target_class: "_User".into(),
},
))
.with_row(
"_Role",
row(vec![("objectId", ParseValue::String("r1".into()))]),
)
.with_row(
"_Role",
row(vec![("objectId", ParseValue::String("r2".into()))]),
)
.with_row(
"_Join:users:_Role",
row(vec![
("relatedId", ParseValue::String("u1".into())),
("owningId", ParseValue::String("r1".into())),
]),
);
let snap = snapshot(&storage).await;
let options = opts();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let results = find(
&ctx,
"_Role",
where_(r#"{"users":{"__type":"Pointer","className":"_User","objectId":"u1"}}"#),
FindOptions::default(),
)
.await
.expect("find");
assert_eq!(results.len(), 1);
assert!(matches!(results[0].get("objectId"), Some(ParseValue::String(id)) if id == "r1"));
}
#[tokio::test]
async fn include_applies_the_target_class_acl() {
let storage = FakeStorage::new()
.with_schema(default_schema("_User").with_field("nickname", FieldType::String))
.with_schema(default_schema("Post").with_field(
"author",
FieldType::Pointer {
target_class: "_User".into(),
},
))
.with_row(
"Post",
row(vec![
("objectId", ParseValue::String("p1".into())),
("author", pointer("_User", "u1")),
]),
)
.with_row(
"_User",
row(vec![
("objectId", ParseValue::String("u1".into())),
("nickname", ParseValue::String("nick".into())),
("_hashed_password", ParseValue::String("hash".into())),
("sessionToken", ParseValue::String("r:t".into())),
("_rperm", strings(&["u1"])),
]),
);
let snap = snapshot(&storage).await;
let options = opts();
let include = FindOptions {
include: vec![vec!["author".to_string()]],
..Default::default()
};
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
let results = find(&ctx, "Post", ParsedWhere::default(), include.clone())
.await
.expect("find");
assert_eq!(results.len(), 1);
assert!(
results[0].get("author").is_none(),
"an unreadable pointer is dropped rather than expanded or left as a pointer"
);
let owner = AclScope::user("u1", vec![]).expect("scope");
let ctx = Ctx::new(&storage, &snap, &owner, &options);
let results = find(&ctx, "Post", ParsedWhere::default(), include)
.await
.expect("find");
match results[0].get("author") {
Some(ParseValue::Object(author)) => {
assert!(
matches!(author.get("nickname"), Some(ParseValue::String(n)) if n == "nick")
);
assert!(author.get("_hashed_password").is_none());
assert!(author.get("sessionToken").is_none());
assert!(
matches!(author.get("__type"), Some(ParseValue::String(t)) if t == "Object")
);
}
other => panic!("expected an expanded author, got {other:?}"),
}
}
#[tokio::test]
async fn a_dotted_include_resolves_parents_before_children() {
let storage = FakeStorage::new()
.with_schema(default_schema("Company").with_field("name", FieldType::String))
.with_schema(default_schema("_User").with_field(
"company",
FieldType::Pointer {
target_class: "Company".into(),
},
))
.with_schema(default_schema("Post").with_field(
"author",
FieldType::Pointer {
target_class: "_User".into(),
},
))
.with_row(
"Post",
row(vec![
("objectId", ParseValue::String("p1".into())),
("author", pointer("_User", "u1")),
]),
)
.with_row(
"_User",
row(vec![
("objectId", ParseValue::String("u1".into())),
("company", pointer("Company", "c1")),
]),
)
.with_row(
"Company",
row(vec![
("objectId", ParseValue::String("c1".into())),
("name", ParseValue::String("Acme".into())),
]),
);
let snap = snapshot(&storage).await;
let options = opts();
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
let results = find(
&ctx,
"Post",
ParsedWhere::default(),
FindOptions {
include: crate::query_parse::parse_include("author.company").expect("include"),
..Default::default()
},
)
.await
.expect("find");
let Some(ParseValue::Object(author)) = results[0].get("author") else {
panic!("author should be expanded");
};
let Some(ParseValue::Object(company)) = author.get("company") else {
panic!("company should be expanded");
};
assert!(matches!(company.get("name"), Some(ParseValue::String(n)) if n == "Acme"));
}
#[tokio::test]
async fn a_client_cannot_bring_a_class_into_existence_at_the_default() {
let storage = FakeStorage::new();
let snap = snapshot(&storage).await;
let options = opts();
let anon = AclScope::Anonymous;
let ctx = Ctx::new(&storage, &snap, &anon, &options);
let err = create(&ctx, "BrandNew", body(r#"{"x":1}"#, OpPath::Create))
.await
.expect_err("a client must not create a class at the default");
assert_eq!(err.code, ErrorCode::OperationForbidden);
assert!(
storage.schema("BrandNew").is_none(),
"the refusal must leave no _SCHEMA row behind"
);
}
#[tokio::test]
async fn master_the_option_and_the_system_classes_are_all_exempt() {
for (label, scope, allow, class) in [
("option on", AclScope::Anonymous, true, "BrandNew"),
("master", AclScope::Unrestricted, false, "BrandNew"),
("system class", AclScope::Anonymous, false, "_User"),
] {
let storage = FakeStorage::new();
let snap = snapshot(&storage).await;
let options = PermissionOptions {
allow_client_class_creation: allow,
..PermissionOptions::default()
};
let ctx = Ctx::new(&storage, &snap, &scope, &options);
create(&ctx, class, body(r#"{"x":1}"#, OpPath::Create))
.await
.unwrap_or_else(|e| panic!("{label} should be allowed to create {class}: {e:?}"));
}
}
}
#[cfg(test)]
mod relation_schema_tests {
use super::tests_support::*;
use super::*;
use crate::testing::FakeStorage;
use parse_rust_core::op::OpPath;
#[tokio::test]
async fn a_relation_op_reserves_the_field_type_before_it_is_stripped() {
let storage = FakeStorage::new()
.with_schema(default_schema("_User"))
.with_row("_User", single("objectId", ParseValue::String("u1".into())));
let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
let options = PermissionOptions::default();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let created = create(
&ctx,
"Team",
decode(
r#"{"name":"core","members":{"__op":"AddRelation","objects":[
{"__type":"Pointer","className":"_User","objectId":"u1"}]}}"#,
OpPath::Create,
),
)
.await
.expect("create");
let schema = storage.schema("Team").expect("class reserved");
assert_eq!(
schema.field("members"),
Some(&FieldType::Relation {
target_class: "_User".into()
}),
"without this a $relatedTo against Team.members resolves to nothing forever"
);
assert!(storage.rows("Team")[0].get("members").is_none());
assert_eq!(storage.rows("_Join:members:Team").len(), 1);
let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
let ctx = Ctx::new(&storage, &snap, &master, &options);
let query = format!(
r#"{{"$relatedTo":{{"object":{{"__type":"Pointer","className":"Team","objectId":"{}"}},"key":"members"}}}}"#,
created.object_id
);
assert_eq!(
find(
&ctx,
"_User",
parse_json_where(&query),
FindOptions::default()
)
.await
.expect("find")
.len(),
1
);
}
}
#[cfg(test)]
mod tests_support {
use super::*;
use crate::query_parse::parse_where;
use crate::write::decode_write_body;
use parse_rust_core::op::OpPath;
pub fn single(key: &str, value: ParseValue) -> ParseMap {
let mut m = ParseMap::new();
m.insert(key.to_string(), value);
m
}
pub fn decode(json: &str, path: OpPath) -> WriteBody {
decode_write_body(&serde_json::from_str(json).expect("test literal"), path).expect("decode")
}
pub fn parse_json_where(json: &str) -> ParsedWhere {
parse_where(&serde_json::from_str(json).expect("test literal")).expect("parse")
}
}
#[cfg(test)]
mod write_edge_tests {
use super::tests_support::*;
use super::*;
use crate::testing::FakeStorage;
use parse_rust_core::op::OpPath;
#[tokio::test]
async fn a_falsy_acl_on_an_update_does_not_clear_the_permissions() {
for body in [
r#"{"ACL":null,"title":"t"}"#,
r#"{"ACL":false,"title":"t"}"#,
r#"{"ACL":0,"title":"t"}"#,
r#"{"ACL":"","title":"t"}"#,
] {
assert_falsy_acl_preserves_permissions(body).await;
}
}
async fn assert_falsy_acl_preserves_permissions(body: &str) {
let mut existing = single("objectId", ParseValue::String("p1".into()));
existing.insert(
"_rperm".to_string(),
ParseValue::Array(vec![ParseValue::String("u1".into())]),
);
existing.insert(
"_wperm".to_string(),
ParseValue::Array(vec![ParseValue::String("u1".into())]),
);
let storage = FakeStorage::new()
.with_schema(default_schema("Post").with_field("title", FieldType::String))
.with_row("Post", existing);
let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
let options = PermissionOptions::default();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
update(&ctx, "Post", "p1", decode(body, OpPath::Update))
.await
.expect("update");
let stored = &storage.rows("Post")[0];
assert!(
matches!(stored.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 1),
"the existing permissions must survive a falsy ACL: {body}"
);
}
#[tokio::test]
async fn an_update_to_a_missing_class_creates_the_class_and_then_finds_nothing() {
for body in [r#"{}"#, r#"{"title":"a"}"#, r#"{"bad-key":1}"#] {
let storage = FakeStorage::new();
let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
let options = PermissionOptions::default();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
let e = update(&ctx, "Ghost", "p1", decode(body, OpPath::Update))
.await
.unwrap_err();
let expected = if body.contains("bad-key") {
ErrorCode::InvalidKeyName
} else {
ErrorCode::ObjectNotFound
};
assert_eq!(e.code, expected, "for body {body}");
assert!(
storage.schema("Ghost").is_some(),
"the schema row survives the failed update, for body {body}"
);
}
}
#[tokio::test]
async fn an_acl_on_an_update_replaces_both_columns() {
let storage = FakeStorage::new()
.with_schema(default_schema("Post"))
.with_row("Post", single("objectId", ParseValue::String("p1".into())));
let snap = SchemaSnapshot::load(&storage).await.expect("snapshot");
let options = PermissionOptions::default();
let master = AclScope::Unrestricted;
let ctx = Ctx::new(&storage, &snap, &master, &options);
update(
&ctx,
"Post",
"p1",
decode(
r#"{"ACL":{"u1":{"read":true,"write":true},"*":{"read":true}}}"#,
OpPath::Update,
),
)
.await
.expect("update");
let stored = &storage.rows("Post")[0];
assert!(stored.get("ACL").is_none(), "ACL is not a stored column");
assert!(matches!(stored.get("_rperm"), Some(ParseValue::Array(a)) if a.len() == 2));
assert!(matches!(stored.get("_wperm"), Some(ParseValue::Array(a)) if a.len() == 1));
}
}