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/// What authority a request carries.
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 Authority {
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. May carry a session token.
33    Client { session_token: Option<String> },
34}
35
36impl Authority {
37    /// True only for the master key. **Not** true for maintenance, and deliberately not a field
38    /// that can be set independently of how the request authenticated.
39    pub fn is_master(&self) -> bool {
40        matches!(self, Authority::Master)
41    }
42}
43
44/// Why a request was refused before reaching a route.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum HeaderRejection {
47    /// Wrong or missing appId, or a required client key was absent or wrong.
48    ///
49    /// Upstream answers all of these identically: HTTP 403, body `{"error":"unauthorized"}`,
50    /// with **no `code` field** (`middlewares.js:829-832`). Collapsing the reasons is
51    /// deliberate upstream, and reproducing it means not adding a more helpful message.
52    Unauthorized,
53}
54
55/// Resolve authority from headers.
56///
57/// Upstream ordering that matters:
58/// 1. The appId must match, else `invalidRequest`.
59/// 2. **Master or maintenance short-circuits**, returning before client-key validation
60///    (`middlewares.js:249-251`). A request carrying both a master key and a session token is a
61///    master request, and the token is not resolved.
62/// 3. Otherwise, if any client key is configured, one must match.
63pub fn resolve(
64    config: &ServerConfig,
65    headers: &http::HeaderMap,
66) -> Result<Authority, HeaderRejection> {
67    let get = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());
68
69    match get(headers::APP_ID) {
70        Some(id) if id == config.app_id => {}
71        _ => return Err(HeaderRejection::Unauthorized),
72    }
73
74    if let Some(k) = get(headers::MASTER_KEY) {
75        if k == config.master_key {
76            return Ok(Authority::Master);
77        }
78    }
79    if let (Some(k), Some(expected)) = (get(headers::MAINTENANCE_KEY), &config.maintenance_key) {
80        if k == expected {
81            return Ok(Authority::Maintenance);
82        }
83    }
84
85    if config.requires_client_key() {
86        let matched = [
87            (get(headers::JAVASCRIPT_KEY), &config.javascript_key),
88            (get(headers::REST_API_KEY), &config.rest_api_key),
89            (get(headers::CLIENT_KEY), &config.client_key),
90            (get(headers::DOT_NET_KEY), &config.dot_net_key),
91        ]
92        .iter()
93        .any(|(presented, expected)| match (presented, expected) {
94            (Some(p), Some(e)) => p == e,
95            _ => false,
96        });
97        if !matched {
98            return Err(HeaderRejection::Unauthorized);
99        }
100    }
101
102    Ok(Authority::Client {
103        session_token: get(headers::SESSION_TOKEN).map(str::to_string),
104    })
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn cfg() -> ServerConfig {
112        ServerConfig::new("app", "master").javascript_key("js")
113    }
114
115    fn hm(pairs: &[(&str, &str)]) -> http::HeaderMap {
116        let mut m = http::HeaderMap::new();
117        for (k, v) in pairs {
118            m.insert(
119                http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
120                http::HeaderValue::from_str(v).unwrap(),
121            );
122        }
123        m
124    }
125
126    #[test]
127    fn master_key_wins_and_short_circuits_client_key_validation() {
128        // A javascript key is configured, but a master request need not present one.
129        let a = resolve(
130            &cfg(),
131            &hm(&[
132                ("x-parse-application-id", "app"),
133                ("x-parse-master-key", "master"),
134            ]),
135        );
136        assert_eq!(a, Ok(Authority::Master));
137    }
138
139    #[test]
140    fn master_key_beats_a_session_token_on_the_same_request() {
141        // Upstream returns before resolving the token. A request carrying both is a master
142        // request, not a user request.
143        let a = resolve(
144            &cfg(),
145            &hm(&[
146                ("x-parse-application-id", "app"),
147                ("x-parse-master-key", "master"),
148                ("x-parse-session-token", "r:tok"),
149            ]),
150        );
151        assert_eq!(a, Ok(Authority::Master));
152        assert!(a.unwrap().is_master());
153    }
154
155    #[test]
156    fn a_configured_client_key_becomes_mandatory() {
157        // Easy to trip over: with a client key configured, omitting it fails with a bare 403
158        // that reads like an authorization problem rather than a missing header.
159        let missing = resolve(&cfg(), &hm(&[("x-parse-application-id", "app")]));
160        assert_eq!(missing, Err(HeaderRejection::Unauthorized));
161
162        let wrong = resolve(
163            &cfg(),
164            &hm(&[
165                ("x-parse-application-id", "app"),
166                ("x-parse-javascript-key", "nope"),
167            ]),
168        );
169        assert_eq!(wrong, Err(HeaderRejection::Unauthorized));
170
171        let right = resolve(
172            &cfg(),
173            &hm(&[
174                ("x-parse-application-id", "app"),
175                ("x-parse-javascript-key", "js"),
176            ]),
177        );
178        assert_eq!(
179            right,
180            Ok(Authority::Client {
181                session_token: None
182            })
183        );
184    }
185
186    #[test]
187    fn no_client_key_configured_means_none_required() {
188        let c = ServerConfig::new("app", "master");
189        let a = resolve(&c, &hm(&[("x-parse-application-id", "app")]));
190        assert_eq!(
191            a,
192            Ok(Authority::Client {
193                session_token: None
194            })
195        );
196    }
197
198    #[test]
199    fn any_one_of_the_configured_keys_suffices() {
200        let c = ServerConfig::new("app", "master")
201            .javascript_key("js")
202            .rest_api_key("rest");
203        for (k, v) in [
204            ("x-parse-javascript-key", "js"),
205            ("x-parse-rest-api-key", "rest"),
206        ] {
207            assert!(resolve(&c, &hm(&[("x-parse-application-id", "app"), (k, v)])).is_ok());
208        }
209    }
210
211    #[test]
212    fn wrong_or_missing_app_id_is_unauthorized() {
213        assert_eq!(
214            resolve(&cfg(), &hm(&[])),
215            Err(HeaderRejection::Unauthorized)
216        );
217        assert_eq!(
218            resolve(&cfg(), &hm(&[("x-parse-application-id", "other")])),
219            Err(HeaderRejection::Unauthorized)
220        );
221    }
222
223    #[test]
224    fn a_wrong_master_key_falls_through_rather_than_short_circuiting() {
225        // It must not be treated as master, and it must not bypass client-key validation.
226        let a = resolve(
227            &cfg(),
228            &hm(&[
229                ("x-parse-application-id", "app"),
230                ("x-parse-master-key", "wrong"),
231            ]),
232        );
233        assert_eq!(a, Err(HeaderRejection::Unauthorized));
234    }
235
236    #[test]
237    fn session_token_is_carried_on_client_authority() {
238        let a = resolve(
239            &cfg(),
240            &hm(&[
241                ("x-parse-application-id", "app"),
242                ("x-parse-javascript-key", "js"),
243                ("x-parse-session-token", "r:abc"),
244            ]),
245        );
246        assert_eq!(
247            a,
248            Ok(Authority::Client {
249                session_token: Some("r:abc".into())
250            })
251        );
252    }
253
254    #[test]
255    fn maintenance_is_not_master() {
256        let mut c = ServerConfig::new("app", "master");
257        c.maintenance_key = Some("maint".into());
258        let a = resolve(
259            &c,
260            &hm(&[
261                ("x-parse-application-id", "app"),
262                ("x-parse-maintenance-key", "maint"),
263            ]),
264        )
265        .unwrap();
266        assert_eq!(a, Authority::Maintenance);
267        assert!(
268            !a.is_master(),
269            "maintenance must not satisfy a master-key gate"
270        );
271    }
272}