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