1use crate::config::ServerConfig;
7
8pub 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#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum Credentials {
28 Master,
30 Maintenance,
32 Client,
34}
35
36#[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 pub fn is_master(&self) -> bool {
60 matches!(self.credentials, Credentials::Master)
61 }
62
63 pub fn is_privileged(&self) -> bool {
65 matches!(
66 self.credentials,
67 Credentials::Master | Credentials::Maintenance
68 )
69 }
70
71 pub fn session_token(&self) -> Option<&str> {
73 self.session_token.as_deref()
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum HeaderRejection {
80 Unauthorized,
86}
87
88pub 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 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 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 assert_eq!(a.session_token(), Some("r:tok"));
207 }
208
209 #[test]
210 fn a_configured_client_key_becomes_mandatory() {
211 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 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 #[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}