Skip to main content

parse_rust_rest/
clp.rs

1//! Class-level permission enforcement.
2//!
3//! **CLP evaluation is two stages, not one.** Stage one, [`validate_permission`], is a gate that
4//! throws. Stage two, [`apply_pointer_permissions`], is a filter that narrows the query. Passing
5//! the gate is not authorization to read anything: `{find: {requiresAuthentication: true,
6//! pointerFields: ['owner']}}` passes the gate for any logged-in user and is still restricted to
7//! that user's own rows by stage two. Conflating them, or treating stage two as optional extra
8//! narrowing, is a data-exposure bug rather than a missing feature.
9//!
10//! Two more shapes here are load bearing:
11//!
12//! - **CLP is default-open.** An absent operation entry means unrestricted. That lives in
13//!   `parse_rust_core::clp`, where `op()` returns `Option<&OpPerm>` and `OpPerm` has no `Default`.
14//!   [`test_permissions`] is the only place that reads the rule, and both stages call it.
15//! - **Deny-all cannot be spelled `None`.** [`PointerPermOutcome`] has three variants and is
16//!   `#[must_use]`, because upstream signals deny-all by returning `undefined` from a function
17//!   that otherwise returns a query (`DatabaseController.js:1770-1772`), and an `Option<Query>`
18//!   reproduces that hazard exactly: `None` reads as "nothing to add".
19//!
20//! Every denial in this module is one of upstream's `createSanitizedError` call sites, so each
21//! one goes through [`ParseError::permission_denied`] and the client sees `Permission denied` at
22//! the default. The detailed strings are still exact, because they are what the wire carries when
23//! `enableSanitizedErrorResponse` is off, and they are what the log carries either way.
24//!
25//! Master and maintenance never reach any of this. Every upstream call site is guarded by
26//! `isMaster ? Promise.resolve() : ...` (`DatabaseController.js:575-578`, `:849-852`, `:935-938`,
27//! `:1471-1474`), and here the guard is the caller matching on [`crate::AclScope::Unrestricted`].
28
29use parse_rust_core::{
30    ClassLevelPermissions, ErrorCode, ErrorDetail, OpEntity, Operation, ParseError, ParseMap,
31    ParseValue, PfEntity, UserFieldsKey,
32};
33use parse_rust_storage::{ClassSchema, Comparison, Constraint, FieldType, Query, SortDirection};
34
35use crate::acl::AclScope;
36use crate::query_parse::ParsedWhere;
37
38/// Which write a permission check belongs to.
39///
40/// Upstream's `runOptions.action` (`DatabaseController.js:994`), which exists only to answer one
41/// question: may this write add a field through a pointer permission? Only an update may.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum WriteAction {
44    Create,
45    Update,
46}
47
48/// Options that change what a permission check decides, and what it says when it denies.
49#[derive(Debug, Clone)]
50pub struct PermissionOptions {
51    /// `protectedFieldsOwnerExempt`. Upstream tests `!== false`, so an unset option is exempt and
52    /// the default is `true` (`DatabaseController.js:1838`).
53    pub protected_fields_owner_exempt: bool,
54    /// `enableSanitizedErrorResponse`, carried here because every denial in this module is one of
55    /// upstream's `createSanitizedError` call sites and needs it.
56    pub error_detail: ErrorDetail,
57    /// `allowClientClassCreation` (`Options/Definitions.js:67-72`).
58    ///
59    /// **The default is `false`, and that is the whole reason this option has to exist rather than
60    /// be left for later.** An unimplemented option is silently the permissive value, and here the
61    /// permissive value lets any caller holding only the app id and client key create classes. Each
62    /// one gets a `_SCHEMA` row and a collection on a database parse-server nodes also read, and
63    /// each has no CLP block, which is default-open. So the gap is not a missing feature, it is a
64    /// security default flipped open.
65    pub allow_client_class_creation: bool,
66}
67
68impl Default for PermissionOptions {
69    fn default() -> Self {
70        Self {
71            protected_fields_owner_exempt: true,
72            allow_client_class_creation: false,
73            // Upstream's default is `enableSanitizedErrorResponse: true`, so the default here has
74            // to be the withholding regime. `ErrorDetail` has no `Default` of its own precisely so
75            // that this choice is written down at the one place a default is legitimate.
76            error_detail: ErrorDetail::Withheld,
77        }
78    }
79}
80
81/// `testPermissions` (`SchemaController.js:1365-1382`).
82///
83/// **Default-open.** No CLP block, or no entry for this operation, means unrestricted. This is
84/// the single most consequential CLP semantic: inverting it fails closed, which looks safe, and
85/// locks every existing database out on upgrade.
86pub fn test_permissions(
87    clp: Option<&ClassLevelPermissions>,
88    acl_group: &[String],
89    operation: Operation,
90) -> bool {
91    let Some(clp) = clp else { return true };
92    let Some(perm) = clp.op(operation) else {
93        return true;
94    };
95    if perm.grants(&OpEntity::Public) {
96        return true;
97    }
98    acl_group.iter().any(|a| perm.grants(&OpEntity::parse(a)))
99}
100
101/// Stage one: the gate. `validatePermission` (`SchemaController.js:1385-1459`).
102///
103/// Never called for master or maintenance.
104///
105/// Both denials are `createSanitizedError` call sites upstream (`SchemaController.js:1406`,
106/// `:1412`, `:1430`, `:1454`), so `detail` decides whether the client is told which rule refused
107/// it or only that something did.
108pub fn validate_permission(
109    clp: Option<&ClassLevelPermissions>,
110    class_name: &str,
111    acl_group: &[String],
112    operation: Operation,
113    action: Option<WriteAction>,
114    detail: ErrorDetail,
115) -> Result<(), ParseError> {
116    // Step 1.
117    if test_permissions(clp, acl_group, operation) {
118        return Ok(());
119    }
120
121    // Step 2 (`SchemaController.js:1396-1398`) re-checks the no-CLP case and returns a bare
122    // `true` rather than a promise. It is unreachable, because step 1 already covered both of
123    // its conditions. Read and deliberately not ported; the two `else { return Ok(()) }` arms
124    // below are the same unreachable case expressed as the resolve it would have produced.
125    let Some(clp) = clp else { return Ok(()) };
126    let Some(perm) = clp.op(operation) else {
127        return Ok(());
128    };
129
130    // Step 3: requiresAuthentication. Note the code: 101, not 119. It is deliberate existence
131    // hiding and it is wire contract.
132    if perm.grants(&OpEntity::RequiresAuthentication) {
133        let anonymous = acl_group.is_empty() || acl_group == ["*"];
134        if anonymous {
135            return Err(ParseError::permission_denied(
136                ErrorCode::ObjectNotFound,
137                "Permission denied, user needs to be authenticated.",
138                detail,
139            ));
140        }
141        // Resolves unconditionally, and precedes the pointer branches. A logged-in caller passes
142        // the gate here and is still narrowed by stage two.
143        return Ok(());
144    }
145
146    // Step 4: a write pointer-permission scheme can never authorize a create, because a create
147    // has no existing object whose pointer field could name the caller.
148    if operation.user_fields_key() == UserFieldsKey::Write && operation == Operation::Create {
149        return Err(forbidden(class_name, operation, detail));
150    }
151
152    // Step 5: defer the class-wide arrays to stage two.
153    if !clp.user_fields(operation).is_empty() {
154        return Ok(());
155    }
156
157    // Step 6: defer per-operation pointerFields to stage two, except when adding a field on a
158    // create. The condition is upstream's `operation !== 'addField' || action === 'update'`.
159    if !perm.pointer_fields.is_empty()
160        && (operation != Operation::AddField || action == Some(WriteAction::Update))
161    {
162        return Ok(());
163    }
164
165    // Step 7.
166    Err(forbidden(class_name, operation, detail))
167}
168
169fn forbidden(class_name: &str, operation: Operation, detail: ErrorDetail) -> ParseError {
170    ParseError::permission_denied(
171        ErrorCode::OperationForbidden,
172        format!(
173            "Permission denied for action {} on class {class_name}.",
174            operation.as_key()
175        ),
176        detail,
177    )
178}
179
180/// Stage two's three outcomes. `#[must_use]`, and every caller matches all three.
181///
182/// See the module note for why this is not `Option<Query>`.
183#[must_use]
184#[derive(Debug)]
185pub enum PointerPermOutcome {
186    /// No pointer permission applies. The query stands as it was.
187    Unconstrained,
188    /// The query, narrowed.
189    Constrained(Query),
190    /// Upstream's `return undefined`. The caller must deny: an empty result for a `find` or a
191    /// `count`, and `OBJECT_NOT_FOUND` for a `get`, an update or a delete.
192    DenyAll,
193}
194
195/// Stage two: the query filter. `addPointerPermissions` (`DatabaseController.js:1731-1819`).
196///
197/// Never called for master or maintenance.
198pub fn apply_pointer_permissions(
199    schema: &ClassSchema,
200    clp: Option<&ClassLevelPermissions>,
201    operation: Operation,
202    acl_group: &[String],
203    query: &Query,
204) -> Result<PointerPermOutcome, ParseError> {
205    // 1. A class the caller can already reach through the base CLP is never pointer-restricted.
206    //    Same predicate as the gate's step 1, deliberately re-evaluated through one function so
207    //    the two stages cannot drift.
208    if test_permissions(clp, acl_group, operation) {
209        return Ok(PointerPermOutcome::Unconstrained);
210    }
211    let Some(clp) = clp else {
212        return Ok(PointerPermOutcome::Unconstrained);
213    };
214
215    // 3. Per-operation pointerFields first, then the class-wide array, deduped in upstream's
216    //    order, which is observable in the compiled `$or`.
217    let fields = clp.applicable_pointer_fields(operation);
218    // 4.
219    if fields.is_empty() {
220        return Ok(PointerPermOutcome::Unconstrained);
221    }
222
223    // 2. The caller's user ids: everything that is neither a role nor the public entity.
224    let user_acl: Vec<&String> = acl_group
225        .iter()
226        .filter(|a| !a.starts_with("role:") && a.as_str() != "*")
227        .collect();
228
229    // 5. The deny-all signal. Fires for every anonymous caller, whose list is empty once `*` is
230    //    filtered out.
231    let [user_id] = user_acl.as_slice() else {
232        return Ok(PointerPermOutcome::DenyAll);
233    };
234
235    let user_pointer = ParseValue::Pointer {
236        class_name: "_User".to_string(),
237        object_id: (*user_id).clone(),
238    };
239
240    // 6. One clause per field, keyed on the schema type rather than on the runtime value.
241    let mut alternatives = Vec::with_capacity(fields.len());
242    for field in &fields {
243        let constraint = match schema.field(field) {
244            Some(FieldType::Pointer { .. }) | Some(FieldType::Object) => {
245                Constraint::equal(field.clone(), user_pointer.clone())
246            }
247            Some(FieldType::Array) => Constraint {
248                field: field.clone(),
249                comparison: Comparison::All(vec![user_pointer.clone()]),
250            },
251            // A CLP naming a field of any other type, or naming no field at all, is a
252            // misconfiguration. Upstream throws a plain `Error` here
253            // (`DatabaseController.js:1803-1805`), which is deliberate: failing open would hand
254            // the whole class to the caller, which is the breach this branch exists to prevent.
255            //
256            // A plain `Error` and not a `Parse.Error`, so the class and field name reach the log
257            // and never the client: `handleParseErrors` renders the fixed
258            // `{"code":1,"message":"Internal server error."}` for anything that is not a
259            // `Parse.Error` (`middlewares.js:636-644`). `ParseError::internal` is that shape.
260            _ => {
261                let class_name = &schema.class_name;
262                return Err(ParseError::internal(format!(
263                    "An unexpected condition occurred when resolving pointer permissions: \
264                     {class_name} {field}"
265                )));
266            }
267        };
268        alternatives.push(Query::from_constraints(vec![constraint]));
269    }
270
271    // 7. Disjunctive across fields (`DatabaseController.js:1815`). `Query::any_of` reproduces
272    //    `reduceOrOperation`'s single-element collapse.
273    //
274    //    Upstream copies the whole incoming query into each disjunct and ORs those; conjoining
275    //    the bare disjunction of clauses onto the query is the same predicate, `q AND (c1 OR
276    //    c2)`, without duplicating the client's constraints into every branch.
277    //
278    //    `conjoin` and not `extend`, because with a single permission field `any_of` collapses to
279    //    a bare constraint on that field and the client may already be constraining it. Upstream
280    //    guards the same case at `DatabaseController.js:1807-1811`.
281    let mut out = query.clone();
282    out.conjoin(Query::any_of(alternatives));
283    Ok(PointerPermOutcome::Constrained(out))
284}
285
286/// `canAddField` (`DatabaseController.js:970-998`): does this write introduce a field the schema
287/// does not have?
288///
289/// `class_exists` is upstream's `if (!classSchema) return`, so a write that creates the class
290/// never runs the `addField` gate at all.
291pub fn adds_field<'a>(
292    schema: &ClassSchema,
293    class_exists: bool,
294    keys: impl IntoIterator<Item = &'a str>,
295    is_delete: impl Fn(&str) -> bool,
296) -> bool {
297    if !class_exists {
298        return false;
299    }
300    keys.into_iter().any(|key| {
301        if is_delete(key) {
302            return false;
303        }
304        // The root of a dotted key: `temperature.celsius` is not a new field if `temperature`
305        // exists, which is why a nested write is exempt in practice.
306        let root = key.split('.').next().unwrap_or(key);
307        schema.field(root).is_none()
308    })
309}
310
311/// The fields to strip from a result, plus the rules that can only be evaluated against a row.
312///
313/// Entirely request state. Nothing derived from a request is ever written back into the schema
314/// snapshot, which is the deliberate divergence from upstream's `temporaryKeys`
315/// (`DatabaseController.js:1907`): that writes into the CLP object held by the shared schema
316/// controller, so two concurrent requests corrupt each other's key list in both directions.
317#[derive(Debug, Clone, Default)]
318pub struct ProtectedFieldPlan {
319    /// Already intersected across every applicable entity.
320    pub strip: Vec<String>,
321    /// `userField:<name>` rules: the field to look at, and what it protects when the row's value
322    /// points at the caller.
323    pub user_field_rules: Vec<(String, Vec<String>)>,
324}
325
326impl ProtectedFieldPlan {
327    pub fn is_empty(&self) -> bool {
328        self.strip.is_empty() && self.user_field_rules.is_empty()
329    }
330}
331
332/// `addProtectedFields` (`DatabaseController.js:1821-1925`).
333///
334/// `pinned_object_id` is the query's top-level `objectId` equality, if it has one. It exists only
335/// for the `_User` owner exemption.
336///
337/// Returns `None` when nothing is protected, which is upstream's `null`.
338pub fn plan_protected_fields(
339    class_name: &str,
340    clp: Option<&ClassLevelPermissions>,
341    scope: &AclScope,
342    pinned_object_id: Option<&str>,
343    options: &PermissionOptions,
344) -> Option<ProtectedFieldPlan> {
345    // 1.
346    let clp = clp?;
347    if clp.protected_fields().is_empty() {
348        return None;
349    }
350
351    // 2. The `_User` owner exemption. Note upstream's triple-equals against `false`: an unset
352    //    option means exempt, so the default is exempt.
353    let acl_group = scope.acl_group();
354    if class_name == "_User"
355        && options.protected_fields_owner_exempt
356        && pinned_object_id.is_some_and(|id| acl_group.iter().any(|a| a == id))
357    {
358        return None;
359    }
360
361    // 3. One set per applicable entity.
362    let authenticated = scope.user_id().is_some();
363    let mut sets: Vec<&Vec<String>> = Vec::new();
364    let mut user_field_rules = Vec::new();
365
366    for (entity, fields) in clp.protected_fields() {
367        match entity {
368            // Deferred: whether it applies cannot be known until the row has been read.
369            PfEntity::UserField(name) => user_field_rules.push((name.clone(), fields.clone())),
370            PfEntity::Public => sets.push(fields),
371            PfEntity::Authenticated if authenticated => sets.push(fields),
372            PfEntity::Role(name) if authenticated && scope.has_role(name) => sets.push(fields),
373            _ => {}
374        }
375    }
376    // The caller's own objectId, if the block names it. Kept out of the loop above because
377    // upstream adds it afterwards (`DatabaseController.js:1898-1903`), and the order of the sets
378    // does not change an intersection.
379    if let Some(user_id) = scope.user_id() {
380        if let Some(fields) = clp
381            .protected_fields()
382            .get(&PfEntity::User(user_id.to_string()))
383        {
384            sets.push(fields);
385        }
386    }
387
388    Some(ProtectedFieldPlan {
389        strip: intersect_all(&sets),
390        user_field_rules,
391    })
392}
393
394/// 4. Intersect every collected set (`DatabaseController.js:1910-1922`).
395///
396/// **More applicable groups means fewer protected fields.** A union over-protects and shows up as
397/// a failing test; a first-match-wins under-protects and shows up as nothing at all.
398fn intersect_all(sets: &[&Vec<String>]) -> Vec<String> {
399    let Some((first, rest)) = sets.split_first() else {
400        return Vec::new();
401    };
402    let mut out: Vec<String> = Vec::new();
403    for field in first.iter() {
404        if out.contains(field) {
405            continue;
406        }
407        if rest.iter().all(|set| set.contains(field)) {
408            out.push(field.clone());
409        }
410    }
411    out
412}
413
414/// `denyProtectedFields` (`RestQuery.js:928-984`).
415///
416/// A pre-flight denial, not a filter. Without it a client binary-searches a protected value
417/// through equality constraints even though the field never appears in a response.
418///
419/// Both denials are `createSanitizedError` call sites (`RestQuery.js:949`, `:976`). Note what
420/// that means at the default: the client learns it was refused, but not which field it named.
421pub fn deny_protected_fields(
422    plan: Option<&ProtectedFieldPlan>,
423    class_name: &str,
424    where_: &ParsedWhere,
425    order: &[(String, SortDirection)],
426    detail: ErrorDetail,
427) -> Result<(), ParseError> {
428    let Some(plan) = plan else { return Ok(()) };
429    if plan.strip.is_empty() {
430        return Ok(());
431    }
432
433    let denied = |key: &str| -> bool {
434        // Checked both as the full key and as its dot-prefix root, so `{"obj.secret": v}` cannot
435        // slip past a protection on `obj`.
436        let root = key.split('.').next().unwrap_or(key);
437        plan.strip.iter().any(|f| f == key || f == root)
438    };
439
440    for key in where_.field_keys() {
441        if denied(&key) {
442            return Err(ParseError::permission_denied(
443                ErrorCode::OperationForbidden,
444                format!("This user is not allowed to query {key} on class {class_name}"),
445                detail,
446            ));
447        }
448    }
449    for (key, _) in order {
450        if denied(key) {
451            return Err(ParseError::permission_denied(
452                ErrorCode::OperationForbidden,
453                format!("This user is not allowed to sort by {key} on class {class_name}"),
454                detail,
455            ));
456        }
457    }
458    Ok(())
459}
460
461/// `filterSensitiveData` (`DatabaseController.js:192-303`), applied to one row in upstream's
462/// order.
463///
464/// `is_read` is upstream's `['get','find'].indexOf(operation) > -1`, which gates the `userField:`
465/// evaluation only.
466///
467/// Two deliberate absences, both stated rather than left implicit:
468///
469/// - **The password hash is never rehydrated under a user-facing name.** Upstream reattaches it
470///   as `password` here (`:266-271`) and strips it at a later stage. parse-rust keeps the hash
471///   under its internal name end to end, so the underscore strip below removes it and there is
472///   nothing for a later stage to undo. That is the point of the divergence: no response path
473///   depends on remembering to remove it, so a new read path cannot acquire the obligation and
474///   miss it.
475/// - **The maintenance bypass (`:273-275`) is not reproduced.** It skips the underscore strip as
476///   well as the protected-field strip, and `AclScope::Unrestricted` covers master and
477///   maintenance together, so parse-rust always strips. Master already strips upstream, so this
478///   only differs for a maintenance caller, and it differs in the direction of removing less
479///   information from nobody.
480pub fn filter_sensitive_data(
481    row: &mut ParseMap,
482    class_name: &str,
483    scope: &AclScope,
484    plan: Option<&ProtectedFieldPlan>,
485    is_read: bool,
486    options: &PermissionOptions,
487) {
488    let is_user_class = class_name == "_User";
489    let acl_group = scope.acl_group();
490
491    // 1. `userField:` matching, against the row rather than the request.
492    let mut protected: Option<Vec<String>> = plan.map(|p| p.strip.clone());
493    if is_read {
494        if let Some(plan) = plan {
495            let matched: Vec<&Vec<String>> = plan
496                .user_field_rules
497                .iter()
498                .filter(|(field, _)| row_field_points_at(row.get(field), scope.user_id()))
499                .map(|(_, fields)| fields)
500                .collect();
501            if !matched.is_empty() {
502                // If a list already exists from the pre-query stage it joins the intersection
503                // rather than being replaced (`DatabaseController.js:248-262`). Matching a
504                // `userField:` rule can therefore only ever protect fewer fields.
505                let mut sets = matched;
506                if let Some(existing) = protected.as_ref() {
507                    sets.push(existing);
508                }
509                protected = Some(intersect_all(&sets));
510            }
511        }
512    }
513
514    // 2. `_User` shaping. See the note above for what is deliberately not here.
515    if is_user_class {
516        row.shift_remove("sessionToken");
517    }
518
519    // 4. Strip the protected fields, unless the caller is the `_User` row's own user.
520    let owner_exempt = options.protected_fields_owner_exempt
521        && is_user_class
522        && scope.user_id().is_some_and(
523            |uid| matches!(row.get("objectId"), Some(ParseValue::String(id)) if id == uid),
524        );
525    if !owner_exempt {
526        if let Some(fields) = protected {
527            for field in fields {
528                row.shift_remove(&field);
529            }
530        }
531    }
532
533    // 5. Every `_`-prefixed key, unconditionally.
534    crate::guard::strip_internal_keys(row);
535
536    // 6. `authData` survives for master and for the object's own user.
537    if !is_user_class || scope.is_master() {
538        return;
539    }
540    let own_row =
541        matches!(row.get("objectId"), Some(ParseValue::String(id)) if acl_group.contains(id));
542    if !own_row {
543        row.shift_remove("authData");
544    }
545}
546
547/// Does this row value point at the caller? A pointer, or an array containing one
548/// (`DatabaseController.js:227-237`).
549fn row_field_points_at(value: Option<&ParseValue>, user_id: Option<&str>) -> bool {
550    let Some(user_id) = user_id else { return false };
551    match value {
552        Some(ParseValue::Pointer { object_id, .. }) => object_id == user_id,
553        Some(ParseValue::Array(items)) => items
554            .iter()
555            .any(|v| matches!(v, ParseValue::Pointer { object_id, .. } if object_id == user_id)),
556        // An `Object`-typed field holding a raw `{objectId: ...}` map, which is what upstream
557        // reads: it inspects `.objectId` without checking `__type`.
558        Some(ParseValue::Object(map)) => {
559            matches!(map.get("objectId"), Some(ParseValue::String(id)) if id == user_id)
560        }
561        _ => false,
562    }
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568    use parse_rust_core::ClassLevelPermissions;
569
570    fn clp(json: &str) -> ClassLevelPermissions {
571        let value = parse_rust_core::classify(
572            serde_json::from_str(json).expect("test literal must be valid JSON"),
573        )
574        .expect("classify");
575        match value {
576            ParseValue::Object(m) => ClassLevelPermissions::from_map(m),
577            _ => panic!("expected an object"),
578        }
579    }
580
581    fn user_scope() -> AclScope {
582        AclScope::user("u1", vec![]).expect("scope")
583    }
584
585    /// The default regime, `enableSanitizedErrorResponse: true`. Named rather than spelled at
586    /// each call site so that a test asserting a detailed message cannot be reading this one.
587    const WITHHELD: ErrorDetail = ErrorDetail::Withheld;
588    /// `enableSanitizedErrorResponse: false`. The detailed strings are contract too: they are
589    /// what the wire carries under this option and what the log carries under either.
590    const DISCLOSED: ErrorDetail = ErrorDetail::Disclosed;
591
592    #[test]
593    fn no_clp_at_all_allows_every_operation() {
594        for op in Operation::ALL {
595            assert!(test_permissions(None, &["*".to_string()], op), "{op:?}");
596            assert!(
597                validate_permission(None, "Post", &["*".to_string()], op, None, DISCLOSED).is_ok()
598            );
599        }
600    }
601
602    /// The rule that locks every existing database out if it is inverted.
603    #[test]
604    fn an_absent_operation_entry_is_unrestricted() {
605        let c = clp(r#"{"find":{}}"#);
606        assert!(
607            validate_permission(
608                Some(&c),
609                "Post",
610                &["*".into()],
611                Operation::Update,
612                None,
613                DISCLOSED
614            )
615            .is_ok(),
616            "update has no entry, so it is unrestricted"
617        );
618        let e = validate_permission(
619            Some(&c),
620            "Post",
621            &["*".into()],
622            Operation::Find,
623            None,
624            DISCLOSED,
625        )
626        .unwrap_err();
627        assert_eq!(e.code, ErrorCode::OperationForbidden);
628        assert_eq!(
629            e.message,
630            "Permission denied for action find on class Post."
631        );
632
633        // The regime a stock deployment runs. The code is unchanged and only the message moves.
634        let withheld = validate_permission(
635            Some(&c),
636            "Post",
637            &["*".into()],
638            Operation::Find,
639            None,
640            WITHHELD,
641        )
642        .unwrap_err();
643        assert_eq!(withheld.code, ErrorCode::OperationForbidden);
644        assert_eq!(withheld.message, "Permission denied");
645    }
646
647    #[test]
648    fn a_granted_role_passes_the_gate() {
649        let c = clp(r#"{"find":{"role:Admins":true}}"#);
650        let member = AclScope::user("u1", vec!["Admins".into()]).expect("scope");
651        assert!(validate_permission(
652            Some(&c),
653            "Post",
654            &member.acl_group(),
655            Operation::Find,
656            None,
657            WITHHELD
658        )
659        .is_ok());
660        let outsider = AclScope::user("u2", vec!["Others".into()]).expect("scope");
661        assert!(validate_permission(
662            Some(&c),
663            "Post",
664            &outsider.acl_group(),
665            Operation::Find,
666            None,
667            WITHHELD
668        )
669        .is_err());
670    }
671
672    /// The code is 101, not 119. Gate C asserts on it.
673    #[test]
674    fn requires_authentication_denies_anonymously_with_object_not_found() {
675        let c = clp(r#"{"find":{"requiresAuthentication":true}}"#);
676        let e = validate_permission(
677            Some(&c),
678            "Post",
679            &AclScope::Anonymous.acl_group(),
680            Operation::Find,
681            None,
682            DISCLOSED,
683        )
684        .unwrap_err();
685        assert_eq!(e.code, ErrorCode::ObjectNotFound);
686        assert_eq!(
687            e.message,
688            "Permission denied, user needs to be authenticated."
689        );
690
691        // At the default the caller is told nothing beyond the refusal, and the code still hides
692        // the class's existence.
693        let withheld = validate_permission(
694            Some(&c),
695            "Post",
696            &AclScope::Anonymous.acl_group(),
697            Operation::Find,
698            None,
699            WITHHELD,
700        )
701        .unwrap_err();
702        assert_eq!(withheld.code, ErrorCode::ObjectNotFound);
703        assert_eq!(withheld.message, "Permission denied");
704
705        // An empty aclGroup denies too, which is the other half of upstream's test.
706        assert_eq!(
707            validate_permission(Some(&c), "Post", &[], Operation::Find, None, WITHHELD)
708                .unwrap_err()
709                .code,
710            ErrorCode::ObjectNotFound
711        );
712
713        assert!(validate_permission(
714            Some(&c),
715            "Post",
716            &user_scope().acl_group(),
717            Operation::Find,
718            None,
719            WITHHELD
720        )
721        .is_ok());
722    }
723
724    /// Passing the gate is not authorization to read anything.
725    #[test]
726    fn requires_authentication_with_pointer_fields_still_narrows_in_stage_two() {
727        let c = clp(r#"{"find":{"requiresAuthentication":true,"pointerFields":["owner"]}}"#);
728        let scope = user_scope();
729        assert!(validate_permission(
730            Some(&c),
731            "Post",
732            &scope.acl_group(),
733            Operation::Find,
734            None,
735            WITHHELD
736        )
737        .is_ok());
738
739        let schema = ClassSchema::new("Post").with_field(
740            "owner",
741            FieldType::Pointer {
742                target_class: "_User".into(),
743            },
744        );
745        match apply_pointer_permissions(
746            &schema,
747            Some(&c),
748            Operation::Find,
749            &scope.acl_group(),
750            &Query::new(),
751        )
752        .expect("no misconfiguration")
753        {
754            PointerPermOutcome::Constrained(q) => assert_eq!(q.clauses.len(), 1),
755            other => panic!("expected Constrained, got {other:?}"),
756        }
757    }
758
759    #[test]
760    fn write_user_fields_lock_down_create_only() {
761        let c = clp(r#"{"create":{},"update":{},"writeUserFields":["owner"]}"#);
762        let scope = user_scope();
763        let e = validate_permission(
764            Some(&c),
765            "Post",
766            &scope.acl_group(),
767            Operation::Create,
768            Some(WriteAction::Create),
769            DISCLOSED,
770        )
771        .unwrap_err();
772        assert_eq!(e.code, ErrorCode::OperationForbidden);
773        assert_eq!(
774            e.message,
775            "Permission denied for action create on class Post."
776        );
777        assert_eq!(
778            validate_permission(
779                Some(&c),
780                "Post",
781                &scope.acl_group(),
782                Operation::Create,
783                Some(WriteAction::Create),
784                WITHHELD,
785            )
786            .unwrap_err()
787            .message,
788            "Permission denied"
789        );
790        assert!(validate_permission(
791            Some(&c),
792            "Post",
793            &scope.acl_group(),
794            Operation::Update,
795            Some(WriteAction::Update),
796            WITHHELD
797        )
798        .is_ok());
799    }
800
801    #[test]
802    fn add_field_defers_only_on_update() {
803        let c = clp(r#"{"addField":{"pointerFields":["owner"]}}"#);
804        let scope = user_scope();
805        assert!(validate_permission(
806            Some(&c),
807            "Post",
808            &scope.acl_group(),
809            Operation::AddField,
810            Some(WriteAction::Update),
811            WITHHELD
812        )
813        .is_ok());
814        assert_eq!(
815            validate_permission(
816                Some(&c),
817                "Post",
818                &scope.acl_group(),
819                Operation::AddField,
820                Some(WriteAction::Create),
821                WITHHELD
822            )
823            .unwrap_err()
824            .code,
825            ErrorCode::OperationForbidden
826        );
827    }
828
829    #[test]
830    fn a_public_class_is_never_pointer_restricted() {
831        let c = clp(r#"{"find":{"*":true,"pointerFields":["owner"]}}"#);
832        let schema = ClassSchema::new("Post");
833        assert!(matches!(
834            apply_pointer_permissions(
835                &schema,
836                Some(&c),
837                Operation::Find,
838                &AclScope::Anonymous.acl_group(),
839                &Query::new()
840            )
841            .expect("ok"),
842            PointerPermOutcome::Unconstrained
843        ));
844    }
845
846    #[test]
847    fn an_anonymous_caller_is_denied_all_by_a_pointer_permission() {
848        let c = clp(r#"{"find":{"pointerFields":["owner"]}}"#);
849        let schema = ClassSchema::new("Post").with_field(
850            "owner",
851            FieldType::Pointer {
852                target_class: "_User".into(),
853            },
854        );
855        assert!(matches!(
856            apply_pointer_permissions(
857                &schema,
858                Some(&c),
859                Operation::Find,
860                &AclScope::Anonymous.acl_group(),
861                &Query::new()
862            )
863            .expect("ok"),
864            PointerPermOutcome::DenyAll
865        ));
866    }
867
868    #[test]
869    fn the_clause_shape_follows_the_schema_type() {
870        let c = clp(r#"{"find":{"pointerFields":["owner","editors","meta"]}}"#);
871        let schema = ClassSchema::new("Post")
872            .with_field(
873                "owner",
874                FieldType::Pointer {
875                    target_class: "_User".into(),
876                },
877            )
878            .with_field("editors", FieldType::Array)
879            .with_field("meta", FieldType::Object);
880        let scope = user_scope();
881        let q = match apply_pointer_permissions(
882            &schema,
883            Some(&c),
884            Operation::Find,
885            &scope.acl_group(),
886            &Query::new(),
887        )
888        .expect("ok")
889        {
890            PointerPermOutcome::Constrained(q) => q,
891            other => panic!("expected Constrained, got {other:?}"),
892        };
893        // Three fields compose disjunctively.
894        match q.clauses.as_slice() {
895            [parse_rust_storage::Clause::Or(alts)] => {
896                assert_eq!(alts.len(), 3);
897                assert!(matches!(
898                    alts[1].clauses[0],
899                    parse_rust_storage::Clause::Field(Constraint {
900                        comparison: Comparison::All(_),
901                        ..
902                    })
903                ));
904            }
905            other => panic!("expected a single Or clause, got {other:?}"),
906        }
907    }
908
909    /// The owner asking for their own rows by name is the ordinary case, not an edge case, and
910    /// splicing the permission constraint in beside the client's produced `INVALID_QUERY`.
911    #[test]
912    fn a_client_constraint_on_the_permission_field_survives_composition() {
913        let c = clp(r#"{"find":{"pointerFields":["owner"]}}"#);
914        let schema = ClassSchema::new("Post").with_field(
915            "owner",
916            FieldType::Pointer {
917                target_class: "_User".into(),
918            },
919        );
920        let scope = user_scope();
921        let client = Query::from_constraints(vec![Constraint::equal(
922            "owner",
923            ParseValue::Pointer {
924                class_name: "_User".into(),
925                object_id: "u1".into(),
926            },
927        )]);
928        let q = match apply_pointer_permissions(
929            &schema,
930            Some(&c),
931            Operation::Find,
932            &scope.acl_group(),
933            &client,
934        )
935        .expect("ok")
936        {
937            PointerPermOutcome::Constrained(q) => q,
938            other => panic!("expected Constrained, got {other:?}"),
939        };
940        // The client's constraint stays where it was and the permission's is nested, so neither
941        // is dropped and the two never merge.
942        assert!(matches!(
943            q.clauses.as_slice(),
944            [
945                parse_rust_storage::Clause::Field(f),
946                parse_rust_storage::Clause::And(nested)
947            ] if f.field == "owner" && nested.len() == 1
948        ));
949    }
950
951    /// Failing open here would hand the whole class to the caller.
952    #[test]
953    fn a_pointer_permission_on_an_unusable_field_type_is_a_500() {
954        let c = clp(r#"{"find":{"pointerFields":["title"]}}"#);
955        let schema = ClassSchema::new("Post").with_field("title", FieldType::String);
956        let e = apply_pointer_permissions(
957            &schema,
958            Some(&c),
959            Operation::Find,
960            &user_scope().acl_group(),
961            &Query::new(),
962        )
963        .unwrap_err();
964        assert_eq!(e.code, ErrorCode::InternalServerError);
965        // The class and the field are in the message, and the message is log-only. Upstream
966        // throws a plain `Error` here for exactly that reason.
967        assert_eq!(e.origin, parse_rust_core::ErrorOrigin::Internal);
968        assert!(e.message.contains("Post"));
969        assert!(e.message.contains("title"));
970
971        // A field the schema does not know about lands in the same arm.
972        let c2 = clp(r#"{"find":{"pointerFields":["nope"]}}"#);
973        assert_eq!(
974            apply_pointer_permissions(
975                &ClassSchema::new("Post"),
976                Some(&c2),
977                Operation::Find,
978                &user_scope().acl_group(),
979                &Query::new()
980            )
981            .unwrap_err()
982            .code,
983            ErrorCode::InternalServerError
984        );
985    }
986
987    #[test]
988    fn protected_fields_intersect_rather_than_union() {
989        let c = clp(
990            r#"{"protectedFields":{"*":["email","phone","ssn"],"authenticated":["email","phone"],"role:A":["email"]}}"#,
991        );
992
993        // Anonymous: only the public tier applies, so everything it names is protected.
994        let anon = plan_protected_fields(
995            "Post",
996            Some(&c),
997            &AclScope::Anonymous,
998            None,
999            &PermissionOptions::default(),
1000        )
1001        .expect("plan");
1002        assert_eq!(anon.strip, vec!["email", "phone", "ssn"]);
1003
1004        // Authenticated: two tiers apply, and the intersection is smaller.
1005        let user = AclScope::user("u1", vec![]).expect("scope");
1006        let plan =
1007            plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
1008                .expect("plan");
1009        assert_eq!(plan.strip, vec!["email", "phone"]);
1010
1011        // Three tiers apply, and it shrinks again. More groups means fewer protected fields.
1012        let admin = AclScope::user("u1", vec!["A".into()]).expect("scope");
1013        let plan = plan_protected_fields(
1014            "Post",
1015            Some(&c),
1016            &admin,
1017            None,
1018            &PermissionOptions::default(),
1019        )
1020        .expect("plan");
1021        assert_eq!(plan.strip, vec!["email"]);
1022    }
1023
1024    /// The other direction: an entity that does not apply must not contribute, or the
1025    /// intersection silently unprotects everything.
1026    #[test]
1027    fn an_inapplicable_role_contributes_nothing() {
1028        let c = clp(r#"{"protectedFields":{"*":["email"],"role:A":["phone"]}}"#);
1029        let outsider = AclScope::user("u1", vec!["B".into()]).expect("scope");
1030        let plan = plan_protected_fields(
1031            "Post",
1032            Some(&c),
1033            &outsider,
1034            None,
1035            &PermissionOptions::default(),
1036        )
1037        .expect("plan");
1038        assert_eq!(
1039            plan.strip,
1040            vec!["email"],
1041            "a role the caller does not hold must not join the intersection"
1042        );
1043    }
1044
1045    #[test]
1046    fn the_user_owner_exemption_defaults_to_exempt_and_honors_the_option() {
1047        let c = clp(r#"{"protectedFields":{"*":["email"]}}"#);
1048        let user = AclScope::user("u1", vec![]).expect("scope");
1049        assert!(
1050            plan_protected_fields(
1051                "_User",
1052                Some(&c),
1053                &user,
1054                Some("u1"),
1055                &PermissionOptions::default()
1056            )
1057            .is_none(),
1058            "an unset option means exempt"
1059        );
1060        let strict = PermissionOptions {
1061            protected_fields_owner_exempt: false,
1062            ..PermissionOptions::default()
1063        };
1064        assert!(plan_protected_fields("_User", Some(&c), &user, Some("u1"), &strict).is_some());
1065        // Another user's row is not exempt.
1066        assert!(plan_protected_fields(
1067            "_User",
1068            Some(&c),
1069            &user,
1070            Some("u2"),
1071            &PermissionOptions::default()
1072        )
1073        .is_some());
1074    }
1075
1076    #[test]
1077    fn a_user_field_rule_matching_the_row_reduces_what_is_stripped() {
1078        let c = clp(r#"{"protectedFields":{"*":["email","phone"],"userField:owner":["phone"]}}"#);
1079        let user = AclScope::user("u1", vec![]).expect("scope");
1080        let plan =
1081            plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
1082                .expect("plan");
1083        assert_eq!(plan.strip, vec!["email", "phone"]);
1084        assert_eq!(plan.user_field_rules.len(), 1);
1085
1086        let mut row = ParseMap::new();
1087        row.insert("objectId".into(), ParseValue::String("p1".into()));
1088        row.insert(
1089            "owner".into(),
1090            ParseValue::Pointer {
1091                class_name: "_User".into(),
1092                object_id: "u1".into(),
1093            },
1094        );
1095        row.insert("email".into(), ParseValue::String("a@b.c".into()));
1096        row.insert("phone".into(), ParseValue::String("555".into()));
1097        filter_sensitive_data(
1098            &mut row,
1099            "Post",
1100            &user,
1101            Some(&plan),
1102            true,
1103            &PermissionOptions::default(),
1104        );
1105        // The matched rule intersects the two lists down to {phone}, so the owner keeps `email`
1106        // and loses `phone`. Matching a `userField:` rule protects fewer fields, not more.
1107        assert!(row.get("email").is_some());
1108        assert!(row.get("phone").is_none());
1109
1110        // A row owned by somebody else keeps the pre-query list, so both are stripped.
1111        let mut other = ParseMap::new();
1112        other.insert(
1113            "owner".into(),
1114            ParseValue::Pointer {
1115                class_name: "_User".into(),
1116                object_id: "u2".into(),
1117            },
1118        );
1119        other.insert("email".into(), ParseValue::String("a@b.c".into()));
1120        other.insert("phone".into(), ParseValue::String("555".into()));
1121        filter_sensitive_data(
1122            &mut other,
1123            "Post",
1124            &user,
1125            Some(&plan),
1126            true,
1127            &PermissionOptions::default(),
1128        );
1129        assert!(other.get("phone").is_none());
1130        assert!(
1131            other.get("email").is_none(),
1132            "no rule matched, so the pre-query list stands and both are stripped"
1133        );
1134    }
1135
1136    /// The direction of a deliberate divergence, pinned so a later change cannot flip it.
1137    ///
1138    /// Upstream temporarily appends the `userField:` name to the projection and records it in
1139    /// `serverOnlyKeys` so the rule can still be evaluated when the client asked for `keys`
1140    /// (`DatabaseController.js:1859-1874`). parse-rust does not, for the reason stated on
1141    /// [`ProtectedFieldPlan`]: that mechanism writes into a memoized schema object upstream and
1142    /// nothing resets it.
1143    ///
1144    /// The consequence is that with `keys` the row carries no `owner` to inspect, the rule cannot
1145    /// match, and the **larger** pre-query strip list stands. That protects more, not less, and
1146    /// that is the whole point of this test: a client asking for fewer fields can never thereby
1147    /// see a field it could not see otherwise.
1148    #[test]
1149    fn a_user_field_rule_that_cannot_be_evaluated_protects_more_not_less() {
1150        let c = clp(r#"{"protectedFields":{"*":["email","phone"],"userField:owner":["phone"]}}"#);
1151        let user = AclScope::user("u1", vec![]).expect("scope");
1152        let plan =
1153            plan_protected_fields("Post", Some(&c), &user, None, &PermissionOptions::default())
1154                .expect("plan");
1155
1156        // With `owner` projected away, exactly as a `keys=email,phone` request would leave it.
1157        let mut projected = ParseMap::new();
1158        projected.insert("email".into(), ParseValue::String("a@b.c".into()));
1159        projected.insert("phone".into(), ParseValue::String("555".into()));
1160        filter_sensitive_data(
1161            &mut projected,
1162            "Post",
1163            &user,
1164            Some(&plan),
1165            true,
1166            &PermissionOptions::default(),
1167        );
1168        assert!(
1169            projected.get("phone").is_none(),
1170            "the rule cannot match without `owner`, so `phone` stays protected"
1171        );
1172        assert!(
1173            projected.get("email").is_none(),
1174            "and so does `email`: the unreduced list is the one that applies"
1175        );
1176
1177        // The same owner, same rule, with `owner` present, keeps `email`. Fewer requested fields
1178        // must never produce more visible ones, so this side has to be the permissive one.
1179        let mut full = ParseMap::new();
1180        full.insert(
1181            "owner".into(),
1182            ParseValue::Pointer {
1183                class_name: "_User".into(),
1184                object_id: "u1".into(),
1185            },
1186        );
1187        full.insert("email".into(), ParseValue::String("a@b.c".into()));
1188        full.insert("phone".into(), ParseValue::String("555".into()));
1189        filter_sensitive_data(
1190            &mut full,
1191            "Post",
1192            &user,
1193            Some(&plan),
1194            true,
1195            &PermissionOptions::default(),
1196        );
1197        assert!(full.get("email").is_some());
1198    }
1199
1200    /// A role whose name is itself principal-shaped.
1201    ///
1202    /// `_Role.name` has no character-set validation upstream, so `role:Admin` is a legal role
1203    /// name and produces the principal `role:role:Admin`. The seam this pins is the one where a
1204    /// refactor "tidies up" the prefixing and silently unwraps one layer, at which point holding
1205    /// the role named `role:Admin` would grant everything ACL'd to `Admin`.
1206    #[test]
1207    fn a_principal_shaped_role_name_is_not_unwrapped() {
1208        let scope = AclScope::user("u1", vec!["role:Admin".to_string()]).expect("scope");
1209        let group = scope.acl_group();
1210        assert!(
1211            group.iter().any(|g| g == "role:role:Admin"),
1212            "the name is prefixed once, not collapsed: {group:?}"
1213        );
1214        assert!(
1215            !group.iter().any(|g| g == "role:Admin"),
1216            "holding a role named `role:Admin` must not grant `Admin`: {group:?}"
1217        );
1218
1219        // And the CLP side agrees: the entry that grants this holder is the doubled one.
1220        let doubled = clp(r#"{"find":{"role:role:Admin":true}}"#);
1221        assert!(test_permissions(
1222            Some(&doubled),
1223            &group,
1224            parse_rust_core::Operation::Find
1225        ));
1226        let single = clp(r#"{"find":{"role:Admin":true}}"#);
1227        assert!(!test_permissions(
1228            Some(&single),
1229            &group,
1230            parse_rust_core::Operation::Find
1231        ));
1232    }
1233
1234    #[test]
1235    fn auth_data_survives_only_for_master_and_the_row_owner() {
1236        let mut row = ParseMap::new();
1237        row.insert("objectId".into(), ParseValue::String("u1".into()));
1238        row.insert("authData".into(), ParseValue::Object(ParseMap::new()));
1239        let owner = AclScope::user("u1", vec![]).expect("scope");
1240        filter_sensitive_data(
1241            &mut row,
1242            "_User",
1243            &owner,
1244            None,
1245            true,
1246            &PermissionOptions::default(),
1247        );
1248        assert!(row.get("authData").is_some());
1249
1250        let mut row = ParseMap::new();
1251        row.insert("objectId".into(), ParseValue::String("u1".into()));
1252        row.insert("authData".into(), ParseValue::Object(ParseMap::new()));
1253        let stranger = AclScope::user("u2", vec![]).expect("scope");
1254        filter_sensitive_data(
1255            &mut row,
1256            "_User",
1257            &stranger,
1258            None,
1259            true,
1260            &PermissionOptions::default(),
1261        );
1262        assert!(row.get("authData").is_none());
1263    }
1264
1265    #[test]
1266    fn the_password_hash_never_reaches_a_response_under_any_name() {
1267        let mut row = ParseMap::new();
1268        row.insert("objectId".into(), ParseValue::String("u1".into()));
1269        row.insert("_hashed_password".into(), ParseValue::String("hash".into()));
1270        row.insert("sessionToken".into(), ParseValue::String("r:t".into()));
1271        filter_sensitive_data(
1272            &mut row,
1273            "_User",
1274            &AclScope::user("u1", vec![]).expect("scope"),
1275            None,
1276            true,
1277            &PermissionOptions::default(),
1278        );
1279        assert!(row.get("_hashed_password").is_none());
1280        assert!(
1281            row.get("password").is_none(),
1282            "the hash is never rehydrated under a user-facing name"
1283        );
1284        assert!(row.get("sessionToken").is_none());
1285    }
1286
1287    #[test]
1288    fn adds_field_ignores_deletes_and_dotted_roots() {
1289        let schema = ClassSchema::new("Post").with_field("meta", FieldType::Object);
1290        assert!(!adds_field(&schema, true, ["meta.x"], |_| false));
1291        assert!(adds_field(&schema, true, ["fresh"], |_| false));
1292        assert!(!adds_field(&schema, true, ["fresh"], |k| k == "fresh"));
1293        assert!(
1294            !adds_field(&schema, false, ["fresh"], |_| false),
1295            "a write that creates the class never runs the addField gate"
1296        );
1297    }
1298}