Skip to main content

parse_rust_server/
auth.rs

1//! Request identity, derived from headers.
2//!
3//! Mirrors the parts of `handleParseHeaders` (`middlewares.js:73-289`) that the current routes
4//! need. The order of checks is upstream's and is load-bearing.
5
6use crate::config::ServerConfig;
7
8/// Header names. Case-insensitive on the wire; `http::HeaderMap` handles that.
9pub mod headers {
10    pub const APP_ID: &str = "x-parse-application-id";
11    pub const MASTER_KEY: &str = "x-parse-master-key";
12    pub const MAINTENANCE_KEY: &str = "x-parse-maintenance-key";
13    pub const JAVASCRIPT_KEY: &str = "x-parse-javascript-key";
14    pub const REST_API_KEY: &str = "x-parse-rest-api-key";
15    pub const CLIENT_KEY: &str = "x-parse-client-key";
16    pub const DOT_NET_KEY: &str = "x-parse-windows-key";
17    pub const SESSION_TOKEN: &str = "x-parse-session-token";
18    pub const INSTALLATION_ID: &str = "x-parse-installation-id";
19}
20
21/// How a request authenticated.
22///
23/// An enum rather than a bag of booleans on purpose. Upstream threads `isMaster` as a boolean
24/// and `acl === undefined` as a master sentinel, and a missed check on either is a fail-open
25/// privilege bug. A caller here has to name the case it is handling.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Credentials {
28    /// Master key presented and matched.
29    Master,
30    /// Maintenance key presented and matched.
31    Maintenance,
32    /// A client key matched, or none was required.
33    Client,
34}
35
36/// What authority a request carries, plus the two headers that are not credentials.
37///
38/// The split mirrors `handleParseHeaders`: `req.auth` decides privilege, while `req.info` carries
39/// the session token and installation id **regardless of how the request authenticated**. Keeping
40/// the token out of [`Credentials`] is what makes that true here: a master request still knows
41/// which token it presented, which is what `GET /sessions/me` reads, while
42/// [`crate::request::resolve`] never looks the token up for a master caller
43/// (`middlewares.js:249-251`).
44///
45/// `installationId` is not a credential and grants nothing. It is carried because exactly one
46/// behavior reads it: `destroyDuplicatedSessions` revokes a user's other sessions for the *same*
47/// installation when a new one is minted (`RestWrite.js:1153`), so a request that drops the
48/// header logs the user in twice on one device.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Authority {
51    pub credentials: Credentials,
52    pub session_token: Option<String>,
53    pub installation_id: Option<String>,
54}
55
56impl Authority {
57    /// True only for the master key. **Not** true for maintenance, and deliberately not a field
58    /// that can be set independently of how the request authenticated.
59    pub fn is_master(&self) -> bool {
60        matches!(self.credentials, Credentials::Master)
61    }
62
63    /// True for master or maintenance, which is the gate every class-security check uses.
64    pub fn is_privileged(&self) -> bool {
65        matches!(
66            self.credentials,
67            Credentials::Master | Credentials::Maintenance
68        )
69    }
70
71    /// The session token this request presented, if any.
72    pub fn session_token(&self) -> Option<&str> {
73        self.session_token.as_deref()
74    }
75}
76
77/// Why a request was refused before reaching a route.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum HeaderRejection {
80    /// Wrong or missing appId, or a required client key was absent or wrong.
81    ///
82    /// Upstream answers all of these identically: HTTP 403, body `{"error":"unauthorized"}`,
83    /// with **no `code` field** (`middlewares.js:829-832`). Collapsing the reasons is
84    /// deliberate upstream, and reproducing it means not adding a more helpful message.
85    Unauthorized,
86}
87
88/// Resolve authority from headers.
89///
90/// Upstream ordering that matters:
91/// 1. The appId must match, else `invalidRequest`.
92/// 2. **Master or maintenance short-circuits**, returning before client-key validation
93///    (`middlewares.js:249-251`). A request carrying both a master key and a session token is a
94///    master request, and the token is not resolved.
95/// 3. Otherwise, if any client key is configured, one must match.
96pub fn resolve(
97    config: &ServerConfig,
98    headers: &http::HeaderMap,
99) -> Result<Authority, HeaderRejection> {
100    let get = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());
101
102    let installation_id = get(headers::INSTALLATION_ID).map(str::to_string);
103    let session_token = get(headers::SESSION_TOKEN).map(str::to_string);
104    let with = |credentials: Credentials| Authority {
105        credentials,
106        session_token: session_token.clone(),
107        installation_id: installation_id.clone(),
108    };
109
110    match get(headers::APP_ID) {
111        Some(id) if id == config.app_id => {}
112        _ => return Err(HeaderRejection::Unauthorized),
113    }
114
115    if let Some(k) = get(headers::MASTER_KEY) {
116        if k == config.master_key {
117            return Ok(with(Credentials::Master));
118        }
119    }
120    if let (Some(k), Some(expected)) = (get(headers::MAINTENANCE_KEY), &config.maintenance_key) {
121        if k == expected {
122            return Ok(with(Credentials::Maintenance));
123        }
124    }
125
126    if config.requires_client_key() {
127        let matched = [
128            (get(headers::JAVASCRIPT_KEY), &config.javascript_key),
129            (get(headers::REST_API_KEY), &config.rest_api_key),
130            (get(headers::CLIENT_KEY), &config.client_key),
131            (get(headers::DOT_NET_KEY), &config.dot_net_key),
132        ]
133        .iter()
134        .any(|(presented, expected)| match (presented, expected) {
135            (Some(p), Some(e)) => p == e,
136            _ => false,
137        });
138        if !matched {
139            return Err(HeaderRejection::Unauthorized);
140        }
141    }
142
143    Ok(with(Credentials::Client))
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn cfg() -> ServerConfig {
151        ServerConfig::new("app", "master").javascript_key("js")
152    }
153
154    fn hm(pairs: &[(&str, &str)]) -> http::HeaderMap {
155        let mut m = http::HeaderMap::new();
156        for (k, v) in pairs {
157            m.insert(
158                http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
159                http::HeaderValue::from_str(v).unwrap(),
160            );
161        }
162        m
163    }
164
165    fn credentials(
166        config: &ServerConfig,
167        pairs: &[(&str, &str)],
168    ) -> Result<Credentials, HeaderRejection> {
169        resolve(config, &hm(pairs)).map(|a| a.credentials)
170    }
171
172    fn anonymous() -> Credentials {
173        Credentials::Client
174    }
175
176    #[test]
177    fn master_key_wins_and_short_circuits_client_key_validation() {
178        // A javascript key is configured, but a master request need not present one.
179        let a = credentials(
180            &cfg(),
181            &[
182                ("x-parse-application-id", "app"),
183                ("x-parse-master-key", "master"),
184            ],
185        );
186        assert_eq!(a, Ok(Credentials::Master));
187    }
188
189    #[test]
190    fn master_key_beats_a_session_token_on_the_same_request() {
191        // Upstream returns before resolving the token. A request carrying both is a master
192        // request, not a user request.
193        let a = resolve(
194            &cfg(),
195            &hm(&[
196                ("x-parse-application-id", "app"),
197                ("x-parse-master-key", "master"),
198                ("x-parse-session-token", "r:tok"),
199            ]),
200        )
201        .unwrap();
202        assert_eq!(a.credentials, Credentials::Master);
203        assert!(a.is_master());
204        // The token is still visible, because `req.info` carries it regardless of privilege.
205        // What master skips is resolving it into a user; see `crate::request::resolve`.
206        assert_eq!(a.session_token(), Some("r:tok"));
207    }
208
209    #[test]
210    fn a_configured_client_key_becomes_mandatory() {
211        // Easy to trip over: with a client key configured, omitting it fails with a bare 403
212        // that reads like an authorization problem rather than a missing header.
213        let missing = credentials(&cfg(), &[("x-parse-application-id", "app")]);
214        assert_eq!(missing, Err(HeaderRejection::Unauthorized));
215
216        let wrong = credentials(
217            &cfg(),
218            &[
219                ("x-parse-application-id", "app"),
220                ("x-parse-javascript-key", "nope"),
221            ],
222        );
223        assert_eq!(wrong, Err(HeaderRejection::Unauthorized));
224
225        let right = credentials(
226            &cfg(),
227            &[
228                ("x-parse-application-id", "app"),
229                ("x-parse-javascript-key", "js"),
230            ],
231        );
232        assert_eq!(right, Ok(anonymous()));
233    }
234
235    #[test]
236    fn no_client_key_configured_means_none_required() {
237        let c = ServerConfig::new("app", "master");
238        assert_eq!(
239            credentials(&c, &[("x-parse-application-id", "app")]),
240            Ok(anonymous())
241        );
242    }
243
244    #[test]
245    fn any_one_of_the_configured_keys_suffices() {
246        let c = ServerConfig::new("app", "master")
247            .javascript_key("js")
248            .rest_api_key("rest");
249        for (k, v) in [
250            ("x-parse-javascript-key", "js"),
251            ("x-parse-rest-api-key", "rest"),
252        ] {
253            assert!(credentials(&c, &[("x-parse-application-id", "app"), (k, v)]).is_ok());
254        }
255    }
256
257    #[test]
258    fn wrong_or_missing_app_id_is_unauthorized() {
259        assert_eq!(credentials(&cfg(), &[]), Err(HeaderRejection::Unauthorized));
260        assert_eq!(
261            credentials(&cfg(), &[("x-parse-application-id", "other")]),
262            Err(HeaderRejection::Unauthorized)
263        );
264    }
265
266    #[test]
267    fn a_wrong_master_key_falls_through_rather_than_short_circuiting() {
268        // It must not be treated as master, and it must not bypass client-key validation.
269        let a = credentials(
270            &cfg(),
271            &[
272                ("x-parse-application-id", "app"),
273                ("x-parse-master-key", "wrong"),
274            ],
275        );
276        assert_eq!(a, Err(HeaderRejection::Unauthorized));
277    }
278
279    #[test]
280    fn session_token_is_carried_on_client_authority() {
281        let a = resolve(
282            &cfg(),
283            &hm(&[
284                ("x-parse-application-id", "app"),
285                ("x-parse-javascript-key", "js"),
286                ("x-parse-session-token", "r:abc"),
287            ]),
288        )
289        .unwrap();
290        assert_eq!(a.session_token(), Some("r:abc"));
291    }
292
293    #[test]
294    fn maintenance_is_not_master() {
295        let mut c = ServerConfig::new("app", "master");
296        c.maintenance_key = Some("maint".into());
297        let a = resolve(
298            &c,
299            &hm(&[
300                ("x-parse-application-id", "app"),
301                ("x-parse-maintenance-key", "maint"),
302            ]),
303        )
304        .unwrap();
305        assert_eq!(a.credentials, Credentials::Maintenance);
306        assert!(
307            !a.is_master(),
308            "maintenance must not satisfy a master-key gate"
309        );
310        assert!(
311            a.is_privileged(),
312            "but it does satisfy the class-security gate"
313        );
314    }
315
316    /// The installation id travels on every authority, not just on a client request. A master-key
317    /// signup mints a session too, and that session's duplicate destruction reads it.
318    #[test]
319    fn the_installation_id_is_carried_regardless_of_how_the_request_authenticated() {
320        for extra in [
321            ("x-parse-master-key", "master"),
322            ("x-parse-javascript-key", "js"),
323        ] {
324            let a = resolve(
325                &cfg(),
326                &hm(&[
327                    ("x-parse-application-id", "app"),
328                    extra,
329                    ("x-parse-installation-id", "inst-1"),
330                ]),
331            )
332            .unwrap();
333            assert_eq!(a.installation_id.as_deref(), Some("inst-1"));
334        }
335    }
336}