Skip to main content

parse_rust_server/
state.rs

1//! Server state: config plus the storage adapter.
2//!
3//! Concrete over `MongoAdapter` rather than generic or boxed. 0.2.0 is Mongo only, and a type
4//! parameter threaded through every handler would be noise until a second backend exists. The
5//! `StorageAdapter` trait is still what the pipelines are written against, so swapping this for a
6//! generic later is a change in one file rather than in every route.
7
8use std::sync::Arc;
9
10use parse_rust_core::ParseError;
11use parse_rust_mongo::MongoAdapter;
12
13use crate::auth::Authority;
14use crate::config::ServerConfig;
15use crate::request::RequestContext;
16
17#[derive(Clone)]
18pub struct AppState {
19    config: Arc<ServerConfig>,
20    storage: Arc<MongoAdapter>,
21}
22
23impl AppState {
24    pub fn new(config: ServerConfig, storage: MongoAdapter) -> Self {
25        Self {
26            config: Arc::new(config),
27            storage: Arc::new(storage),
28        }
29    }
30
31    pub fn config(&self) -> &ServerConfig {
32        &self.config
33    }
34
35    pub fn storage(&self) -> &MongoAdapter {
36        &self.storage
37    }
38
39    /// Create the indexes parse-server creates at boot.
40    ///
41    /// **The names are contract, not housekeeping.** Both adapters recover `duplicated_field` by
42    /// regex over the index name, and the Mongo regex matches only auto-generated `<field>_1`
43    /// names, so passing `None` here (which lets the driver auto-name) is what makes a username
44    /// collision surface as 202 `USERNAME_TAKEN` rather than a bare 137. Naming them ourselves
45    /// would silently change the error a client sees.
46    ///
47    /// Upstream gates each of these behind a `databaseOptions.createIndex*` flag
48    /// (`DatabaseController.js:1981-2038`). Only `createIndexRoleName` is modeled; the two
49    /// `_User` indexes are unconditional here, which is what their flags default to.
50    pub async fn ensure_indexes(&self) -> Result<(), ParseError> {
51        use parse_rust_storage::StorageAdapter;
52        self.storage
53            .ensure_index("_User", &["username"], None, true, false)
54            .await?;
55        self.storage
56            .ensure_index("_User", &["email"], None, true, false)
57            .await?;
58        // **The case-insensitive pair, and note they are not unique**
59        // (`DatabaseController.js:1988-2005`). Upstream's `ensureIndex` never sets `unique`, so
60        // these exist to make the collated uniqueness *query* fast, not to enforce anything. The
61        // enforcement is the query in `validate_user_identity`.
62        //
63        // Creating them unique looks stricter and is a mixed-fleet break: parse-server booting
64        // against the same database asks for the non-unique form under the same name, gets
65        // `IndexKeySpecsConflict` (86), and refuses to start. Gate D found exactly that.
66        //
67        // Named rather than auto-named, because upstream names them and a mixed fleet has to agree
68        // on what exists.
69        self.storage
70            .ensure_index(
71                "_User",
72                &["username"],
73                Some("case_insensitive_username"),
74                false,
75                true,
76            )
77            .await?;
78        self.storage
79            .ensure_index(
80                "_User",
81                &["email"],
82                Some("case_insensitive_email"),
83                false,
84                true,
85            )
86            .await?;
87        // `_Role.name`, `ensureUniqueness('_Role', requiredRoleFields, ['name'])`
88        // (`DatabaseController.js:2033-2038`). Upstream passes no index name, so Mongo
89        // auto-generates `name_1`, which is the form the `duplicated_field` regex matches.
90        //
91        // Without it two `_Role` rows can share a name, and an ACL entry of `role:X` then grants
92        // every member of both. That is a privilege-escalation path, not a data-hygiene one.
93        if self.config.create_index_role_name {
94            self.storage
95                .ensure_index("_Role", &["name"], None, true, false)
96                .await?;
97        }
98        Ok(())
99    }
100
101    /// Resolve the request context: session, roles, ACL scope and the schema snapshot.
102    ///
103    /// Called **once** per HTTP request, including a `/batch` whose sub-requests then share it.
104    pub async fn request_context(
105        &self,
106        authority: &Authority,
107    ) -> Result<RequestContext, ParseError> {
108        crate::request::resolve(&self.storage, &self.config, authority).await
109    }
110}
111
112impl axum::extract::FromRef<AppState> for Arc<ServerConfig> {
113    fn from_ref(state: &AppState) -> Self {
114        state.config.clone()
115    }
116}