parse_rust_server/config.rs
1//! Server configuration.
2//!
3//! A deliberately small slice of upstream's ~292 options: what `/serverInfo` and the header
4//! layer actually need. Options are added when a route needs one, not speculatively, so that
5//! every field here has a behavior behind it.
6
7/// The parse-server version parse-rust reports as its own.
8///
9/// **This is a decision, not an oversight.** `/serverInfo` returns `parseServerVersion`, and
10/// SDKs branch on it: the Ruby SDK warns below 7.0.0, and features gate on version comparisons.
11/// Reporting `parse-rust 0.0.0` would fail every one of those checks, so the wire-compatible
12/// answer is the parse-server version whose behavior this server implements. It is the same
13/// number recorded in `PIN`.
14///
15/// If parse-rust ever needs to advertise itself distinctly, that belongs in a separate field
16/// that upstream does not define, not in this one.
17pub const REPORTED_PARSE_SERVER_VERSION: &str = "9.10.1-alpha.6";
18
19/// What this server can actually do, as reported by `GET /serverInfo`.
20///
21/// **The key set and the nesting of the `features` object are wire contract. The booleans are
22/// not.** Upstream hardcodes nearly all of them to `true` (`FeaturesRouter.js`) because upstream
23/// implements the subsystems behind them. Transcribing those literals would advertise a schema
24/// API, cloud jobs, hooks, a global config and a log API that all answer 404 here.
25///
26/// That matters because the object is not documentation: Parse Dashboard builds its UI from it,
27/// so an advertised capability becomes a button that fails when a user presses it. This is a
28/// deliberate difference from upstream, in the direction of telling the truth. Every field is
29/// `false` until the subsystem behind it exists, and flipping one is part of landing that
30/// subsystem rather than a follow-up.
31#[derive(Debug, Clone, Default)]
32pub struct FeatureSupport {
33 /// `/config`. Not implemented.
34 pub global_config: bool,
35 /// `/hooks`. Not implemented.
36 pub hooks: bool,
37 /// Cloud Code jobs. Not implemented; `TriggerHost` is the milestone that lands them.
38 pub cloud_code_jobs: bool,
39 /// The log API. Not implemented.
40 pub logs: bool,
41 /// The schema API, `/schemas`. Not implemented. Note this is the *API*: schema inference
42 /// and enforcement do work, but no route exposes them.
43 pub schemas: bool,
44 /// Push, including audiences and localization. Not implemented.
45 pub push_audiences: bool,
46}
47
48/// The keys and identity a request is checked against.
49#[derive(Debug, Clone)]
50pub struct ServerConfig {
51 pub app_id: String,
52 pub master_key: String,
53 /// The read-only master key sets `isMaster` upstream (`Auth.js:63`), with the restriction
54 /// enforced by scattered checks. Not implemented yet; recorded so the gap is visible.
55 pub maintenance_key: Option<String>,
56 pub javascript_key: Option<String>,
57 pub rest_api_key: Option<String>,
58 pub client_key: Option<String>,
59 pub dot_net_key: Option<String>,
60 /// Where the API is mounted, e.g. `/parse`. A **builder input, never inferred from the
61 /// request path**: axum's `nest` and Express's `app.use` differ here, and every generated
62 /// file URL is built from this value.
63 pub mount_path: String,
64 /// `enableSanitizedErrorResponse`, default true (`Options/Definitions.js:253-258`). When
65 /// true, a 403 from the master-key gate says `Permission denied` rather than naming the
66 /// reason.
67 pub enable_sanitized_error_response: bool,
68 pub has_push_support: bool,
69 pub has_push_scheduled_support: bool,
70 pub security_check_enabled: bool,
71 /// What `/serverInfo` advertises. Defaults to the truth: nothing unimplemented.
72 pub features: FeatureSupport,
73}
74
75impl ServerConfig {
76 pub fn new(app_id: impl Into<String>, master_key: impl Into<String>) -> Self {
77 Self {
78 app_id: app_id.into(),
79 master_key: master_key.into(),
80 maintenance_key: None,
81 javascript_key: None,
82 rest_api_key: None,
83 client_key: None,
84 dot_net_key: None,
85 mount_path: "/parse".to_string(),
86 enable_sanitized_error_response: true,
87 has_push_support: false,
88 has_push_scheduled_support: false,
89 security_check_enabled: false,
90 features: FeatureSupport::default(),
91 }
92 }
93
94 pub fn javascript_key(mut self, k: impl Into<String>) -> Self {
95 self.javascript_key = Some(k.into());
96 self
97 }
98
99 pub fn rest_api_key(mut self, k: impl Into<String>) -> Self {
100 self.rest_api_key = Some(k.into());
101 self
102 }
103
104 pub fn mount_path(mut self, p: impl Into<String>) -> Self {
105 self.mount_path = p.into();
106 self
107 }
108
109 /// True when any client key is configured. Upstream's rule is all-or-nothing: if *any* of
110 /// these is set, a non-master request must present one that matches
111 /// (`middlewares.js:255-265`). If none is configured, none is required.
112 pub fn requires_client_key(&self) -> bool {
113 self.javascript_key.is_some()
114 || self.rest_api_key.is_some()
115 || self.client_key.is_some()
116 || self.dot_net_key.is_some()
117 }
118}