1use std::net::IpAddr;
7
8use crate::config::ServerConfig;
9
10pub mod headers {
12 pub const APP_ID: &str = "x-parse-application-id";
13 pub const MASTER_KEY: &str = "x-parse-master-key";
14 pub const MAINTENANCE_KEY: &str = "x-parse-maintenance-key";
15 pub const JAVASCRIPT_KEY: &str = "x-parse-javascript-key";
16 pub const REST_API_KEY: &str = "x-parse-rest-api-key";
17 pub const CLIENT_KEY: &str = "x-parse-client-key";
18 pub const DOT_NET_KEY: &str = "x-parse-windows-key";
19 pub const SESSION_TOKEN: &str = "x-parse-session-token";
20 pub const INSTALLATION_ID: &str = "x-parse-installation-id";
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Credentials {
30 Master,
32 Maintenance,
34 Client,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Authority {
53 pub credentials: Credentials,
54 pub session_token: Option<String>,
55 pub installation_id: Option<String>,
56}
57
58impl Authority {
59 pub fn is_master(&self) -> bool {
62 matches!(self.credentials, Credentials::Master)
63 }
64
65 pub fn is_privileged(&self) -> bool {
67 matches!(
68 self.credentials,
69 Credentials::Master | Credentials::Maintenance
70 )
71 }
72
73 pub fn session_token(&self) -> Option<&str> {
75 self.session_token.as_deref()
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum HeaderRejection {
82 Unauthorized,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum Peer {
99 Address(IpAddr),
101 Unknown,
103}
104
105impl Peer {
106 fn allowed_by(self, allowlist: &crate::ip_allowlist::IpAllowlist) -> bool {
107 match self {
108 Peer::Address(ip) => allowlist.allows(ip),
109 Peer::Unknown => false,
110 }
111 }
112}
113
114impl From<std::net::SocketAddr> for Peer {
115 fn from(addr: std::net::SocketAddr) -> Self {
116 Peer::Address(addr.ip())
117 }
118}
119
120pub fn resolve_with_peer(
141 config: &ServerConfig,
142 headers: &http::HeaderMap,
143 peer: Peer,
144) -> Result<Authority, HeaderRejection> {
145 let get = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());
146
147 let installation_id = get(headers::INSTALLATION_ID).map(str::to_string);
148 let session_token = get(headers::SESSION_TOKEN).map(str::to_string);
149 let with = |credentials: Credentials| Authority {
150 credentials,
151 session_token: session_token.clone(),
152 installation_id: installation_id.clone(),
153 };
154
155 match get(headers::APP_ID) {
156 Some(id) if id == config.app_id => {}
157 _ => return Err(HeaderRejection::Unauthorized),
158 }
159
160 if let (Some(k), Some(expected)) = (get(headers::MAINTENANCE_KEY), &config.maintenance_key) {
166 if k == expected {
167 if !peer.allowed_by(&config.maintenance_key_ips) {
168 return Err(HeaderRejection::Unauthorized);
169 }
170 return Ok(with(Credentials::Maintenance));
171 }
172 }
173 if let Some(k) = get(headers::MASTER_KEY) {
174 if k == config.master_key {
175 if !peer.allowed_by(&config.master_key_ips) {
176 return Err(HeaderRejection::Unauthorized);
177 }
178 return Ok(with(Credentials::Master));
179 }
180 }
181
182 if config.requires_client_key() {
183 let matched = [
184 (get(headers::JAVASCRIPT_KEY), &config.javascript_key),
185 (get(headers::REST_API_KEY), &config.rest_api_key),
186 (get(headers::CLIENT_KEY), &config.client_key),
187 (get(headers::DOT_NET_KEY), &config.dot_net_key),
188 ]
189 .iter()
190 .any(|(presented, expected)| match (presented, expected) {
191 (Some(p), Some(e)) => p == e,
192 _ => false,
193 });
194 if !matched {
195 return Err(HeaderRejection::Unauthorized);
196 }
197 }
198
199 Ok(with(Credentials::Client))
200}
201
202#[deprecated(
215 since = "0.2.1",
216 note = "the master key is filtered by source address; call resolve_with_peer. \
217 This form refuses every master and maintenance key because it has no address to check."
218)]
219pub fn resolve(
220 config: &ServerConfig,
221 headers: &http::HeaderMap,
222) -> Result<Authority, HeaderRejection> {
223 resolve_with_peer(config, headers, Peer::Unknown)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 fn cfg() -> ServerConfig {
231 ServerConfig::new("app", "master").javascript_key("js")
232 }
233
234 fn hm(pairs: &[(&str, &str)]) -> http::HeaderMap {
235 let mut m = http::HeaderMap::new();
236 for (k, v) in pairs {
237 m.insert(
238 http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
239 http::HeaderValue::from_str(v).unwrap(),
240 );
241 }
242 m
243 }
244
245 fn loopback() -> Peer {
248 Peer::Address("127.0.0.1".parse().expect("loopback"))
249 }
250
251 fn from(address: &str) -> Peer {
252 Peer::Address(address.parse().expect("test address"))
253 }
254
255 fn resolve_from(
256 config: &ServerConfig,
257 pairs: &[(&str, &str)],
258 peer: Peer,
259 ) -> Result<Authority, HeaderRejection> {
260 resolve_with_peer(config, &hm(pairs), peer)
261 }
262
263 fn credentials(
264 config: &ServerConfig,
265 pairs: &[(&str, &str)],
266 ) -> Result<Credentials, HeaderRejection> {
267 resolve_with_peer(config, &hm(pairs), loopback()).map(|a| a.credentials)
268 }
269
270 fn anonymous() -> Credentials {
271 Credentials::Client
272 }
273
274 #[test]
275 fn master_key_wins_and_short_circuits_client_key_validation() {
276 let a = credentials(
278 &cfg(),
279 &[
280 ("x-parse-application-id", "app"),
281 ("x-parse-master-key", "master"),
282 ],
283 );
284 assert_eq!(a, Ok(Credentials::Master));
285 }
286
287 #[test]
288 fn master_key_beats_a_session_token_on_the_same_request() {
289 let a = resolve_with_peer(
292 &cfg(),
293 &hm(&[
294 ("x-parse-application-id", "app"),
295 ("x-parse-master-key", "master"),
296 ("x-parse-session-token", "r:tok"),
297 ]),
298 loopback(),
299 )
300 .unwrap();
301 assert_eq!(a.credentials, Credentials::Master);
302 assert!(a.is_master());
303 assert_eq!(a.session_token(), Some("r:tok"));
306 }
307
308 #[test]
309 fn a_configured_client_key_becomes_mandatory() {
310 let missing = credentials(&cfg(), &[("x-parse-application-id", "app")]);
313 assert_eq!(missing, Err(HeaderRejection::Unauthorized));
314
315 let wrong = credentials(
316 &cfg(),
317 &[
318 ("x-parse-application-id", "app"),
319 ("x-parse-javascript-key", "nope"),
320 ],
321 );
322 assert_eq!(wrong, Err(HeaderRejection::Unauthorized));
323
324 let right = credentials(
325 &cfg(),
326 &[
327 ("x-parse-application-id", "app"),
328 ("x-parse-javascript-key", "js"),
329 ],
330 );
331 assert_eq!(right, Ok(anonymous()));
332 }
333
334 #[test]
335 fn no_client_key_configured_means_none_required() {
336 let c = ServerConfig::new("app", "master");
337 assert_eq!(
338 credentials(&c, &[("x-parse-application-id", "app")]),
339 Ok(anonymous())
340 );
341 }
342
343 #[test]
344 fn any_one_of_the_configured_keys_suffices() {
345 let c = ServerConfig::new("app", "master")
346 .javascript_key("js")
347 .rest_api_key("rest");
348 for (k, v) in [
349 ("x-parse-javascript-key", "js"),
350 ("x-parse-rest-api-key", "rest"),
351 ] {
352 assert!(credentials(&c, &[("x-parse-application-id", "app"), (k, v)]).is_ok());
353 }
354 }
355
356 #[test]
357 fn wrong_or_missing_app_id_is_unauthorized() {
358 assert_eq!(credentials(&cfg(), &[]), Err(HeaderRejection::Unauthorized));
359 assert_eq!(
360 credentials(&cfg(), &[("x-parse-application-id", "other")]),
361 Err(HeaderRejection::Unauthorized)
362 );
363 }
364
365 #[test]
366 fn a_wrong_master_key_falls_through_rather_than_short_circuiting() {
367 let a = credentials(
369 &cfg(),
370 &[
371 ("x-parse-application-id", "app"),
372 ("x-parse-master-key", "wrong"),
373 ],
374 );
375 assert_eq!(a, Err(HeaderRejection::Unauthorized));
376 }
377
378 #[test]
379 fn session_token_is_carried_on_client_authority() {
380 let a = resolve_with_peer(
381 &cfg(),
382 &hm(&[
383 ("x-parse-application-id", "app"),
384 ("x-parse-javascript-key", "js"),
385 ("x-parse-session-token", "r:abc"),
386 ]),
387 loopback(),
388 )
389 .unwrap();
390 assert_eq!(a.session_token(), Some("r:abc"));
391 }
392
393 #[test]
394 fn maintenance_is_not_master() {
395 let mut c = ServerConfig::new("app", "master");
396 c.maintenance_key = Some("maint".into());
397 let a = resolve_with_peer(
398 &c,
399 &hm(&[
400 ("x-parse-application-id", "app"),
401 ("x-parse-maintenance-key", "maint"),
402 ]),
403 loopback(),
404 )
405 .unwrap();
406 assert_eq!(a.credentials, Credentials::Maintenance);
407 assert!(
408 !a.is_master(),
409 "maintenance must not satisfy a master-key gate"
410 );
411 assert!(
412 a.is_privileged(),
413 "but it does satisfy the class-security gate"
414 );
415 }
416
417 #[test]
420 fn the_installation_id_is_carried_regardless_of_how_the_request_authenticated() {
421 for extra in [
422 ("x-parse-master-key", "master"),
423 ("x-parse-javascript-key", "js"),
424 ] {
425 let a = resolve_with_peer(
426 &cfg(),
427 &hm(&[
428 ("x-parse-application-id", "app"),
429 extra,
430 ("x-parse-installation-id", "inst-1"),
431 ]),
432 loopback(),
433 )
434 .unwrap();
435 assert_eq!(a.installation_id.as_deref(), Some("inst-1"));
436 }
437 }
438
439 const MASTER: [(&str, &str); 2] = [
444 ("x-parse-application-id", "app"),
445 ("x-parse-master-key", "master"),
446 ];
447
448 #[test]
451 fn a_master_key_from_a_non_allowlisted_address_is_refused_at_the_default() {
452 let c = ServerConfig::new("app", "master");
453 for peer in ["127.0.0.2", "10.0.0.5", "203.0.113.9", "2001:db8::1"] {
454 assert_eq!(
455 resolve_from(&c, &MASTER, from(peer)),
456 Err(HeaderRejection::Unauthorized),
457 "{peer} must not be able to use the master key at the default"
458 );
459 }
460 }
461
462 #[test]
464 fn a_master_key_from_loopback_still_works_at_the_default() {
465 let c = ServerConfig::new("app", "master");
466 for peer in ["127.0.0.1", "::1", "::ffff:127.0.0.1"] {
467 assert_eq!(
468 resolve_from(&c, &MASTER, from(peer)).map(|a| a.credentials),
469 Ok(Credentials::Master),
470 "{peer} is the machine the server runs on"
471 );
472 }
473 }
474
475 #[test]
478 fn adding_the_address_admits_the_same_request() {
479 let mut c = ServerConfig::new("app", "master");
480 c.master_key_ips =
481 crate::ip_allowlist::IpAllowlist::parse(["127.0.0.1", "::1", "127.0.0.2"])
482 .expect("entries");
483 assert_eq!(
484 resolve_from(&c, &MASTER, from("127.0.0.2")).map(|a| a.credentials),
485 Ok(Credentials::Master)
486 );
487 }
488
489 #[test]
493 fn a_refused_master_key_does_not_fall_through_to_the_client_key() {
494 let c = ServerConfig::new("app", "master").javascript_key("js");
495 let with_client_key = [
496 ("x-parse-application-id", "app"),
497 ("x-parse-master-key", "master"),
498 ("x-parse-javascript-key", "js"),
499 ];
500 assert_eq!(
501 resolve_from(&c, &with_client_key, from("10.0.0.5")),
502 Err(HeaderRejection::Unauthorized)
503 );
504 }
505
506 #[test]
509 fn a_wrong_master_key_is_unaffected_by_the_allowlist() {
510 let c = ServerConfig::new("app", "master").javascript_key("js");
511 let wrong = [
512 ("x-parse-application-id", "app"),
513 ("x-parse-master-key", "nope"),
514 ("x-parse-javascript-key", "js"),
515 ];
516 assert_eq!(
517 resolve_from(&c, &wrong, from("10.0.0.5")).map(|a| a.credentials),
518 Ok(Credentials::Client)
519 );
520 }
521
522 #[test]
525 fn the_allowlist_does_not_touch_an_ordinary_client_request() {
526 let c = ServerConfig::new("app", "master").javascript_key("js");
527 let client = [
528 ("x-parse-application-id", "app"),
529 ("x-parse-javascript-key", "js"),
530 ];
531 assert_eq!(
532 resolve_from(&c, &client, from("203.0.113.9")).map(|a| a.credentials),
533 Ok(Credentials::Client)
534 );
535 }
536
537 #[test]
539 fn an_empty_allowlist_refuses_loopback_too() {
540 let mut c = ServerConfig::new("app", "master");
541 c.master_key_ips = crate::ip_allowlist::IpAllowlist::deny_all();
542 for peer in ["127.0.0.1", "::1"] {
543 assert_eq!(
544 resolve_from(&c, &MASTER, from(peer)),
545 Err(HeaderRejection::Unauthorized)
546 );
547 }
548 }
549
550 #[test]
553 fn the_maintenance_key_is_filtered_the_same_way() {
554 let mut c = ServerConfig::new("app", "master");
555 c.maintenance_key = Some("maint".into());
556 let maint = [
557 ("x-parse-application-id", "app"),
558 ("x-parse-maintenance-key", "maint"),
559 ];
560 assert_eq!(
561 resolve_from(&c, &maint, loopback()).map(|a| a.credentials),
562 Ok(Credentials::Maintenance)
563 );
564 assert_eq!(
565 resolve_from(&c, &maint, from("10.0.0.5")),
566 Err(HeaderRejection::Unauthorized)
567 );
568 }
569
570 #[test]
574 fn an_unknown_peer_cannot_present_a_privileged_key() {
575 let mut c = ServerConfig::new("app", "master");
576 c.maintenance_key = Some("maint".into());
577 assert_eq!(
578 resolve_from(&c, &MASTER, Peer::Unknown),
579 Err(HeaderRejection::Unauthorized)
580 );
581 assert_eq!(
582 resolve_from(
583 &c,
584 &[
585 ("x-parse-application-id", "app"),
586 ("x-parse-maintenance-key", "maint"),
587 ],
588 Peer::Unknown,
589 ),
590 Err(HeaderRejection::Unauthorized)
591 );
592 assert_eq!(
595 resolve_from(&c, &[("x-parse-application-id", "app")], Peer::Unknown)
596 .map(|a| a.credentials),
597 Ok(Credentials::Client)
598 );
599 }
600
601 #[test]
606 fn both_keys_on_one_request_resolve_to_maintenance() {
607 let mut c = ServerConfig::new("app", "master");
608 c.maintenance_key = Some("maint".into());
609 let both = [
610 ("x-parse-application-id", "app"),
611 ("x-parse-master-key", "master"),
612 ("x-parse-maintenance-key", "maint"),
613 ];
614 assert_eq!(
615 resolve_from(&c, &both, loopback()).map(|a| a.credentials),
616 Ok(Credentials::Maintenance)
617 );
618 }
619
620 #[test]
624 fn with_both_keys_the_maintenance_allowlist_is_the_one_that_decides() {
625 let both = [
626 ("x-parse-application-id", "app"),
627 ("x-parse-master-key", "master"),
628 ("x-parse-maintenance-key", "maint"),
629 ];
630
631 let mut refusing = ServerConfig::new("app", "master");
633 refusing.maintenance_key = Some("maint".into());
634 refusing.maintenance_key_ips = crate::ip_allowlist::IpAllowlist::deny_all();
635 assert_eq!(
636 resolve_from(&refusing, &both, loopback()),
637 Err(HeaderRejection::Unauthorized),
638 "the maintenance allowlist decides, so a master key on the same request cannot rescue it"
639 );
640
641 let mut allowing = ServerConfig::new("app", "master");
643 allowing.maintenance_key = Some("maint".into());
644 allowing.master_key_ips = crate::ip_allowlist::IpAllowlist::deny_all();
645 assert_eq!(
646 resolve_from(&allowing, &both, loopback()).map(|a| a.credentials),
647 Ok(Credentials::Maintenance),
648 "and a refused master key on the same request does not taint an allowed maintenance one"
649 );
650 }
651
652 #[test]
658 fn a_forwarded_header_moves_a_caller_neither_in_nor_out() {
659 let c = ServerConfig::new("app", "master");
660 let forged = [
661 ("x-parse-application-id", "app"),
662 ("x-parse-master-key", "master"),
663 ("x-forwarded-for", "127.0.0.1"),
664 ];
665 assert_eq!(
666 resolve_from(&c, &forged, from("10.0.0.5")),
667 Err(HeaderRejection::Unauthorized),
668 "a client-supplied header must not admit a non-allowlisted peer"
669 );
670
671 let pointing_out = [
672 ("x-parse-application-id", "app"),
673 ("x-parse-master-key", "master"),
674 ("x-forwarded-for", "10.0.0.5"),
675 ];
676 assert_eq!(
677 resolve_from(&c, &pointing_out, loopback()).map(|a| a.credentials),
678 Ok(Credentials::Master),
679 "and it must not evict an allowlisted one either"
680 );
681 }
682}