Skip to main content

parse_rust_server/
config.rs

1//! Server configuration.
2//!
3//! A deliberately small slice of upstream's ~292 options: what the routes that exist actually
4//! need. Options are added when a route needs one, not speculatively, so that every field here
5//! has a behavior behind it. Each carries upstream's env var name and upstream's default; a
6//! wrong default in this file is a security default.
7
8use indexmap::IndexMap;
9use parse_rust_auth::SessionConfig;
10use parse_rust_core::ErrorDetail;
11use parse_rust_rest::PermissionOptions;
12use parse_rust_schema::{ClpValidation, ObjectIdForm, Unenforceable};
13
14use crate::ip_allowlist::IpAllowlist;
15
16/// The parse-server version parse-rust reports as its own.
17///
18/// **This is a decision, not an oversight.** `/serverInfo` returns `parseServerVersion`, and
19/// SDKs branch on it: the Ruby SDK warns below 7.0.0, and features gate on version comparisons.
20/// Reporting `parse-rust 0.0.0` would fail every one of those checks, so the wire-compatible
21/// answer is the parse-server version whose behavior this server implements. It is the same
22/// number recorded in `PIN`.
23///
24/// If parse-rust ever needs to advertise itself distinctly, that belongs in a separate field
25/// that upstream does not define, not in this one.
26pub const REPORTED_PARSE_SERVER_VERSION: &str = "9.10.1-alpha.6";
27
28/// What this server can actually do, as reported by `GET /serverInfo`.
29///
30/// **The key set and the nesting of the `features` object are wire contract. The booleans are
31/// not.** Upstream hardcodes nearly all of them to `true` (`FeaturesRouter.js`) because upstream
32/// implements the subsystems behind them. Transcribing those literals would advertise a schema
33/// API, cloud jobs, hooks, a global config and a log API that all answer 404 here.
34///
35/// That matters because the object is not documentation: Parse Dashboard builds its UI from it,
36/// so an advertised capability becomes a button that fails when a user presses it. This is a
37/// deliberate difference from upstream, in the direction of telling the truth. Every field is
38/// `false` until the subsystem behind it exists, and flipping one is part of landing that
39/// subsystem rather than a follow-up.
40#[derive(Debug, Clone)]
41pub struct FeatureSupport {
42    /// `/config`. Not implemented.
43    pub global_config: bool,
44    /// `/hooks`. Not implemented.
45    pub hooks: bool,
46    /// Cloud Code jobs. Not implemented; `TriggerHost` is the milestone that lands them.
47    pub cloud_code_jobs: bool,
48    /// The log API. Not implemented.
49    pub logs: bool,
50    /// The schema API, `/schemas` and `DELETE /purge/:className`.
51    ///
52    /// True as of 0.2.0, and every capability it drives has a route that does the thing:
53    /// `addField` and `removeField` through `PUT`, `addClass` through `POST`, `removeClass`
54    /// through `DELETE`, `clearAllDataFromClass` through `DELETE /purge/:className`,
55    /// `editClassLevelPermissions` through the `classLevelPermissions` key on `POST` and `PUT`,
56    /// and `editPointerPermissions` through per-operation `pointerFields` plus the class-wide
57    /// `readUserFields` and `writeUserFields` arrays, all three of which the read and write
58    /// pipelines enforce.
59    pub schemas: bool,
60    /// Push, including audiences and localization. Not implemented.
61    pub push_audiences: bool,
62}
63
64impl Default for FeatureSupport {
65    fn default() -> Self {
66        Self {
67            global_config: false,
68            hooks: false,
69            cloud_code_jobs: false,
70            logs: false,
71            schemas: true,
72            push_audiences: false,
73        }
74    }
75}
76
77/// Server-level `protectedFields`: class name, then entity, then the fields that entity may not
78/// see (`Options/Definitions.js:491-500`).
79///
80/// Order-preserving because the intersection that consumes it is order-sensitive on the wire.
81pub type ProtectedFieldsConfig = IndexMap<String, IndexMap<String, Vec<String>>>;
82
83/// The upstream default, `{_User: {'*': ['email']}}` (`Options/Definitions.js:495-499`).
84///
85/// **Merged as a set union per entity key over the class's own block**
86/// (`SchemaController.js:577-586`), so it composes with a configured `protectedFields` rather
87/// than replacing it. A class that protects `phone` from `*` ends up protecting `phone` and
88/// `email`, which is what a parse-server node reading the same database would do.
89pub fn default_protected_fields() -> ProtectedFieldsConfig {
90    let mut entities = IndexMap::new();
91    entities.insert("*".to_string(), vec!["email".to_string()]);
92    let mut classes = ProtectedFieldsConfig::new();
93    classes.insert("_User".to_string(), entities);
94    classes
95}
96
97/// Fold the defaults into a configured `protectedFields`, as upstream does at option-resolution
98/// time (`ParseServer.ts:657-673`).
99///
100/// **A configured block adds to the defaults, it does not replace them.** Assigning the parsed
101/// configuration straight onto the config is the obvious translation and it is a data exposure: a
102/// deployment that configures protection for one of its own classes and never mentions `_User`
103/// thereby unprotects `email` on every user, which the operator did not ask for and cannot see in
104/// their own configuration file.
105///
106/// Upstream's rule, per class present in the defaults:
107///
108/// - the configuration does not name the class at all, so the default block is used whole;
109/// - the configuration names it, so each default entity key is unioned into the configured one.
110///
111/// The single exception is `protectedFieldsOwnerExempt == false`, where a configured entity key is
112/// left exactly as written. That option means "apply `protectedFields` to the owner the same as to
113/// anyone else", and merging a default the operator did not write would undo the point of setting
114/// it.
115pub fn merge_protected_fields_defaults(configured: &mut ProtectedFieldsConfig, owner_exempt: bool) {
116    for (class_name, default_entities) in default_protected_fields() {
117        let Some(entities) = configured.get_mut(&class_name) else {
118            configured.insert(class_name, default_entities);
119            continue;
120        };
121        for (entity, default_fields) in default_entities {
122            match entities.get_mut(&entity) {
123                // Configured and the owner is not exempt: upstream returns early and the
124                // configured list stands alone.
125                Some(_) if !owner_exempt => {}
126                Some(fields) => {
127                    for field in default_fields {
128                        if !fields.contains(&field) {
129                            fields.push(field);
130                        }
131                    }
132                }
133                None => {
134                    entities.insert(entity, default_fields);
135                }
136            }
137        }
138    }
139}
140
141/// The keys and identity a request is checked against.
142///
143/// **`#[non_exhaustive]`, decided at 0.2.1 rather than inherited.** This release adds two public
144/// fields, which already breaks any `ServerConfig { .. }` literal outside this crate, and cargo
145/// resolves 0.2.1 as compatible with 0.2.0 and will upgrade into it unasked. Marking it here means
146/// the break happens once, in the release that was going to cause it anyway, instead of again
147/// every time an option is added. Construction is [`ServerConfig::new`] followed by field
148/// assignment, which is what every call site in this repository already does and what the
149/// attribute still permits.
150#[derive(Debug, Clone)]
151#[non_exhaustive]
152pub struct ServerConfig {
153    pub app_id: String,
154    pub master_key: String,
155
156    /// `masterKeyIps`, default `['127.0.0.1', '::1']` (`Options/Definitions.js:396-399`), enforced
157    /// at `middlewares.js:452`.
158    ///
159    /// **The default is a control and 0.2.0 shipped without it**, so the master key was honoured
160    /// from any source address on a server nobody had configured. A master key presented from an
161    /// address outside this list is refused outright rather than downgraded to a client request:
162    /// upstream throws a bare 403 (`middlewares.js:453-462`) instead of falling through.
163    pub master_key_ips: IpAllowlist,
164
165    /// `maintenanceKey`. Grants the same ACL treatment as the master key and is **not** the same
166    /// authority: `validateClientClassCreation` exempts both on a write and master alone on a read
167    /// (`RestWrite.js:200-202`, `RestQuery.js:486-489`).
168    ///
169    /// **Reachable only from Rust, deliberately.** The binary exposes no variable for it, and the
170    /// reason changed in 0.2.1: it used to be that parse-rust had no IP filter, and now it has one.
171    /// What remains is that master and maintenance are one `AclScope` internally, so every decision
172    /// other than the one corrected above treats them alike. Shipping the key through the CLI would
173    /// advertise an authority this server only partly distinguishes.
174    ///
175    /// Not to be confused with the **read-only** master key, which is a third credential, sets
176    /// `isMaster` upstream (`Auth.js:63`), and is not modeled at all.
177    pub maintenance_key: Option<String>,
178
179    /// `maintenanceKeyIps`, same default and same enforcement (`Options/Definitions.js:385-388`,
180    /// `middlewares.js:438`).
181    ///
182    /// Carried alongside `master_key_ips` rather than deferred. The two options are one mechanism
183    /// with two call sites, and filtering one key while leaving the other unfiltered would close a
184    /// hole and leave its twin open one header away. The exposure is narrower, because a
185    /// maintenance key has no default value and only exists once an operator sets one.
186    pub maintenance_key_ips: IpAllowlist,
187    pub javascript_key: Option<String>,
188    pub rest_api_key: Option<String>,
189    pub client_key: Option<String>,
190    pub dot_net_key: Option<String>,
191    /// Where the API is mounted, e.g. `/parse`. A **builder input, never inferred from the
192    /// request path**: axum's `nest` and Express's `app.use` differ here, and every generated
193    /// file URL is built from this value.
194    pub mount_path: String,
195    /// `enableSanitizedErrorResponse`, default true (`Options/Definitions.js:253-258`).
196    ///
197    /// When true, every denial upstream routes through `createSanitizedError` or
198    /// `createSanitizedHttpError` (`Error.js:13-43`) says `Permission denied` instead of naming
199    /// the rule that refused. That is the configuration an unmodified deployment runs, so it is
200    /// what every SDK sees by default. Read it as [`ServerConfig::error_detail`] rather than as a
201    /// bare bool at a call site.
202    pub enable_sanitized_error_response: bool,
203    pub has_push_support: bool,
204    pub has_push_scheduled_support: bool,
205    pub security_check_enabled: bool,
206    /// What `/serverInfo` advertises. Defaults to the truth: nothing unimplemented.
207    pub features: FeatureSupport,
208
209    /// `sessionLength` and `expireInactiveSessions`, which together decide `_Session.expiresAt`.
210    /// Defaults are upstream's (`Options/Definitions.js:629-634`, `:269-274`).
211    pub session: SessionConfig,
212
213    /// `protectedFields`. See [`default_protected_fields`] for the merge rule.
214    pub protected_fields: ProtectedFieldsConfig,
215
216    /// `protectedFieldsOwnerExempt`, default true (`Options/Definitions.js:501-506`). When true a
217    /// user reading their own `_User` row sees every field regardless of `protectedFields`.
218    pub protected_fields_owner_exempt: bool,
219
220    /// `protectedFieldsSaveResponseExempt`, default true (`Options/Definitions.js:507-512`).
221    ///
222    /// When true, a create or update response carries protected fields the write touched. When
223    /// false they are stripped from the response as they are from a query result. parse-rust only
224    /// ever echoes back the keys whose request value was an operation, so this narrows that echo
225    /// rather than a whole object.
226    pub protected_fields_save_response_exempt: bool,
227
228    /// `allowCustomObjectId`, default false (`Options/Definitions.js:73-78`).
229    ///
230    /// Two effects, and the second is easy to forget because it is in a different file. It gates
231    /// whether a create may carry its own `objectId` (`RestWrite.js:50-65`, enforced by
232    /// `enforce_object_id_policy`), **and** it widens the objectId grammar a CLP entity key is
233    /// matched against, from `^[a-zA-Z0-9]{1,}$` to `^.{1,}$` (`SchemaController.js:726-731`).
234    ///
235    /// The two are one option because a CLP naming a user by id has to be able to name a user
236    /// whose id the client chose.
237    pub allow_custom_object_id: bool,
238
239    /// `allowClientClassCreation`, default **false** (`Options/Definitions.js:67-72`).
240    ///
241    /// Gates whether a write may bring a class into existence. Enforced by
242    /// `validateClientClassCreation` in the write pipeline, which exempts master, maintenance and
243    /// the classes Parse defines itself.
244    ///
245    /// The default matters more than the option. Left unimplemented, a server behaves as though
246    /// this were `true`, which lets a caller holding only the app id and client key create classes
247    /// without limit on a database parse-server nodes also read, each with a default-open CLP.
248    pub allow_client_class_creation: bool,
249
250    /// `allowOrigin`, default `["*"]` (`middlewares.js:407-408`).
251    ///
252    /// A list rather than one value, because upstream accepts either and echoes back whichever
253    /// entry matches the request's `Origin`. An unmatched origin gets the first entry, so a
254    /// single-element list is an allowlist of one rather than a wildcard.
255    pub allow_origin: Vec<String>,
256
257    /// `allowHeaders`. Appended to `DEFAULT_ALLOWED_HEADERS` rather than replacing it
258    /// (`middlewares.js:402-405`), so a deployment adding one custom header does not have to
259    /// restate the twelve a Parse SDK needs.
260    pub allow_headers: Vec<String>,
261
262    /// `requestComplexity.batchRequestLimit`, default `-1`, which disables it
263    /// (`Options/Definitions.js:733-738`). Master and maintenance bypass it (`batch.js:73`).
264    pub batch_request_limit: i64,
265
266    /// `databaseOptions.createIndexRoleName`, default true (`Options/Definitions.js:1318-1323`),
267    /// created at `DatabaseController.js:2033-2038`.
268    ///
269    /// **Not cosmetic.** Without the index two `_Role` rows can carry the same `name`, and an ACL
270    /// entry of `role:X` then grants every member of both, which is a privilege-escalation path
271    /// rather than a duplicate-data annoyance. Upstream tests `!== false`, so anything other than
272    /// an explicit `false` creates it.
273    pub create_index_role_name: bool,
274}
275
276impl ServerConfig {
277    pub fn new(app_id: impl Into<String>, master_key: impl Into<String>) -> Self {
278        Self {
279            app_id: app_id.into(),
280            master_key: master_key.into(),
281            master_key_ips: IpAllowlist::default(),
282            maintenance_key: None,
283            maintenance_key_ips: IpAllowlist::default(),
284            javascript_key: None,
285            rest_api_key: None,
286            client_key: None,
287            dot_net_key: None,
288            mount_path: "/parse".to_string(),
289            enable_sanitized_error_response: true,
290            has_push_support: false,
291            has_push_scheduled_support: false,
292            security_check_enabled: false,
293            features: FeatureSupport::default(),
294            session: SessionConfig::default(),
295            protected_fields: default_protected_fields(),
296            protected_fields_owner_exempt: true,
297            protected_fields_save_response_exempt: true,
298            allow_custom_object_id: false,
299            allow_client_class_creation: false,
300            allow_origin: vec!["*".to_string()],
301            allow_headers: Vec::new(),
302            batch_request_limit: -1,
303            create_index_role_name: true,
304        }
305    }
306
307    /// Whether a denial tells the client why.
308    ///
309    /// The one place `enable_sanitized_error_response` becomes an [`ErrorDetail`], so no call site
310    /// has to remember which way round the bool runs.
311    pub fn error_detail(&self) -> ErrorDetail {
312        ErrorDetail::from_sanitized(self.enable_sanitized_error_response)
313    }
314
315    /// How a CLP entity key that looks like an objectId is matched.
316    pub fn object_id_form(&self) -> ObjectIdForm {
317        if self.allow_custom_object_id {
318            ObjectIdForm::Custom
319        } else {
320            ObjectIdForm::Generated
321        }
322    }
323
324    /// What CLP validation accepts.
325    ///
326    /// `Unenforceable::Accept`, because the read and write pipelines enforce all three of the
327    /// features the toggle guards: per-operation `pointerFields`, the class-wide `readUserFields`
328    /// and `writeUserFields` arrays (`ClassLevelPermissions::applicable_pointer_fields`), and
329    /// `userField:` protected-field entries (`ProtectedFieldPlan::user_field_rules`). Refusing
330    /// them would reject a CLP this server honors.
331    pub fn clp_validation(&self) -> ClpValidation {
332        ClpValidation {
333            object_id: self.object_id_form(),
334            unenforceable: Unenforceable::Accept,
335        }
336    }
337
338    /// The permission options the read and write pipelines take.
339    pub fn permission_options(&self) -> PermissionOptions {
340        PermissionOptions {
341            protected_fields_owner_exempt: self.protected_fields_owner_exempt,
342            error_detail: self.error_detail(),
343            allow_client_class_creation: self.allow_client_class_creation,
344        }
345    }
346
347    pub fn javascript_key(mut self, k: impl Into<String>) -> Self {
348        self.javascript_key = Some(k.into());
349        self
350    }
351
352    pub fn rest_api_key(mut self, k: impl Into<String>) -> Self {
353        self.rest_api_key = Some(k.into());
354        self
355    }
356
357    pub fn mount_path(mut self, p: impl Into<String>) -> Self {
358        self.mount_path = p.into();
359        self
360    }
361
362    /// True when any client key is configured. Upstream's rule is all-or-nothing: if *any* of
363    /// these is set, a non-master request must present one that matches
364    /// (`middlewares.js:255-265`). If none is configured, none is required.
365    pub fn requires_client_key(&self) -> bool {
366        self.javascript_key.is_some()
367            || self.rest_api_key.is_some()
368            || self.client_key.is_some()
369            || self.dot_net_key.is_some()
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    fn fields(config: &ProtectedFieldsConfig, class: &str, entity: &str) -> Vec<String> {
378        config
379            .get(class)
380            .and_then(|e| e.get(entity))
381            .cloned()
382            .unwrap_or_default()
383    }
384
385    /// The exposure this merge exists to prevent. A deployment protecting one of its own classes
386    /// and never mentioning `_User` must still protect `email`, or every user's address becomes
387    /// readable by every other user without that appearing anywhere in the configuration.
388    #[test]
389    fn configuring_an_unrelated_class_still_protects_user_email() {
390        let mut configured = ProtectedFieldsConfig::new();
391        let mut post = IndexMap::new();
392        post.insert("*".to_string(), vec!["secret".to_string()]);
393        configured.insert("Post".to_string(), post);
394
395        merge_protected_fields_defaults(&mut configured, true);
396
397        assert_eq!(fields(&configured, "_User", "*"), vec!["email".to_string()]);
398        assert_eq!(fields(&configured, "Post", "*"), vec!["secret".to_string()]);
399    }
400
401    /// Present but for a different entity key: the default `*` is added alongside rather than
402    /// displacing what was configured.
403    #[test]
404    fn a_user_block_for_another_entity_gains_the_default_star() {
405        let mut configured = ProtectedFieldsConfig::new();
406        let mut user = IndexMap::new();
407        user.insert("authenticated".to_string(), vec!["phone".to_string()]);
408        configured.insert("_User".to_string(), user);
409
410        merge_protected_fields_defaults(&mut configured, true);
411
412        assert_eq!(fields(&configured, "_User", "*"), vec!["email".to_string()]);
413        assert_eq!(
414            fields(&configured, "_User", "authenticated"),
415            vec!["phone".to_string()]
416        );
417    }
418
419    /// Same entity key: a set union, and `email` is not duplicated if it was already named.
420    #[test]
421    fn the_same_entity_key_is_unioned_without_duplicating() {
422        let mut configured = ProtectedFieldsConfig::new();
423        let mut user = IndexMap::new();
424        user.insert(
425            "*".to_string(),
426            vec!["phone".to_string(), "email".to_string()],
427        );
428        configured.insert("_User".to_string(), user);
429
430        merge_protected_fields_defaults(&mut configured, true);
431
432        assert_eq!(
433            fields(&configured, "_User", "*"),
434            vec!["phone".to_string(), "email".to_string()]
435        );
436    }
437
438    /// `protectedFieldsOwnerExempt == false` is the one case where a configured entity key stands
439    /// alone (`ParseServer.ts:664-666`). The operator asked for their list to apply to everyone
440    /// including the owner, so a default they did not write is not folded in.
441    #[test]
442    fn owner_exempt_false_leaves_a_configured_entity_key_alone() {
443        let mut configured = ProtectedFieldsConfig::new();
444        let mut user = IndexMap::new();
445        user.insert("*".to_string(), vec!["phone".to_string()]);
446        configured.insert("_User".to_string(), user);
447
448        merge_protected_fields_defaults(&mut configured, false);
449
450        assert_eq!(fields(&configured, "_User", "*"), vec!["phone".to_string()]);
451    }
452
453    /// But an absent class is still filled in wholesale even then: upstream's early return is
454    /// reached only when the entity key is already present.
455    #[test]
456    fn owner_exempt_false_still_fills_in_an_absent_class() {
457        let mut configured = ProtectedFieldsConfig::new();
458        let mut post = IndexMap::new();
459        post.insert("*".to_string(), vec!["secret".to_string()]);
460        configured.insert("Post".to_string(), post);
461
462        merge_protected_fields_defaults(&mut configured, false);
463
464        assert_eq!(fields(&configured, "_User", "*"), vec!["email".to_string()]);
465    }
466}