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}