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 std::net::IpAddr;
7
8use crate::config::ServerConfig;
9
10/// Header names. Case-insensitive on the wire; `http::HeaderMap` handles that.
11pub 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/// How a request authenticated.
24///
25/// An enum rather than a bag of booleans on purpose. Upstream threads `isMaster` as a boolean
26/// and `acl === undefined` as a master sentinel, and a missed check on either is a fail-open
27/// privilege bug. A caller here has to name the case it is handling.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Credentials {
30    /// Master key presented and matched.
31    Master,
32    /// Maintenance key presented and matched.
33    Maintenance,
34    /// A client key matched, or none was required.
35    Client,
36}
37
38/// What authority a request carries, plus the two headers that are not credentials.
39///
40/// The split mirrors `handleParseHeaders`: `req.auth` decides privilege, while `req.info` carries
41/// the session token and installation id **regardless of how the request authenticated**. Keeping
42/// the token out of [`Credentials`] is what makes that true here: a master request still knows
43/// which token it presented, which is what `GET /sessions/me` reads, while
44/// [`crate::request::resolve`] never looks the token up for a master caller
45/// (`middlewares.js:249-251`).
46///
47/// `installationId` is not a credential and grants nothing. It is carried because exactly one
48/// behavior reads it: `destroyDuplicatedSessions` revokes a user's other sessions for the *same*
49/// installation when a new one is minted (`RestWrite.js:1153`), so a request that drops the
50/// header logs the user in twice on one device.
51#[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    /// True only for the master key. **Not** true for maintenance, and deliberately not a field
60    /// that can be set independently of how the request authenticated.
61    pub fn is_master(&self) -> bool {
62        matches!(self.credentials, Credentials::Master)
63    }
64
65    /// True for master or maintenance, which is the gate every class-security check uses.
66    pub fn is_privileged(&self) -> bool {
67        matches!(
68            self.credentials,
69            Credentials::Master | Credentials::Maintenance
70        )
71    }
72
73    /// The session token this request presented, if any.
74    pub fn session_token(&self) -> Option<&str> {
75        self.session_token.as_deref()
76    }
77}
78
79/// Why a request was refused before reaching a route.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum HeaderRejection {
82    /// Wrong or missing appId, or a required client key was absent or wrong.
83    ///
84    /// Upstream answers all of these identically: HTTP 403, body `{"error":"unauthorized"}`,
85    /// with **no `code` field** (`middlewares.js:829-832`). Collapsing the reasons is
86    /// deliberate upstream, and reproducing it means not adding a more helpful message.
87    Unauthorized,
88}
89
90/// Where a request came from, as far as the allowlist is concerned.
91///
92/// **An enum rather than an `Option<IpAddr>`, because the two cases fail in opposite directions**
93/// and the one that is easy to write is the wrong one. `Unknown` means the transport did not
94/// supply a peer address, which happens when an embedder mounts [`crate::router`] into its own
95/// axum app without `into_make_service_with_connect_info`. A privileged key is refused in that
96/// case: an allowlist that cannot see the address it is filtering on has not been satisfied.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum Peer {
99    /// The address the socket reports.
100    Address(IpAddr),
101    /// No peer address is available. Every privileged key is refused.
102    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
120/// Resolve authority from headers and the peer address.
121///
122/// Upstream ordering that matters:
123/// 1. The appId must match, else `invalidRequest`.
124/// 2. **Master or maintenance short-circuits**, returning before client-key validation
125///    (`middlewares.js:249-251`). A request carrying both a master key and a session token is a
126///    master request, and the token is not resolved.
127/// 3. Otherwise, if any client key is configured, one must match.
128///
129/// **The peer address is the socket's, never a header's.** Upstream's `getClientIp` is `req.ip`
130/// (`middlewares.js:358-360`), and Express resolves that from the connection unless `trust proxy`
131/// is set, which parse-server does not set. An allowlist that reads `X-Forwarded-For` is not an
132/// allowlist, because the caller writes it. A deployment behind a load balancer therefore sees
133/// every request as coming from the balancer and must widen the option; that is an availability
134/// failure rather than an authorization one, and it is the correct direction to fail in until
135/// trusted-proxy configuration exists.
136///
137/// A matching master key from a non-allowlisted address is **refused**, not demoted to a client
138/// request. Upstream throws (`middlewares.js:453-462`) rather than falling through, so a caller
139/// cannot use a rejected master key to skip client-key validation.
140pub 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    // **Maintenance is tested first, and the order is upstream's** (`resolveKeyAuth`,
161    // `middlewares.js:436-455`). It was reversed here until the two keys grew separate address
162    // allowlists, at which point it became observable: a request carrying both valid keys from an
163    // address allowed for one and not the other resolves to a different authority on each server,
164    // or is accepted by one and refused by the other.
165    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/// The 0.2.0 signature, kept so a patch release does not stop a downstream crate compiling.
203///
204/// **It fails closed rather than preserving 0.2.0's behavior**, which is the point: 0.2.0's
205/// behavior here is the defect. With no peer address there is nothing to check `masterKeyIps`
206/// against, so the master and maintenance keys are refused and every other request is unaffected.
207/// A caller that wants them to work has to say where the request came from, which is
208/// [`resolve_with_peer`].
209///
210/// Cargo treats 0.2.1 as compatible with 0.2.0 and will upgrade into it without being asked, so
211/// removing this would turn a security patch into a build failure for anyone calling `resolve`
212/// directly. Keeping it means their build still succeeds and their privileged keys stop working
213/// until they pass an address, which is loud at runtime and safe in the meantime.
214#[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    /// The address the whole existing battery speaks from, and the one the shipped default
246    /// allows. Tests about the allowlist itself name their address explicitly.
247    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        // A javascript key is configured, but a master request need not present one.
277        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        // Upstream returns before resolving the token. A request carrying both is a master
290        // request, not a user request.
291        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        // The token is still visible, because `req.info` carries it regardless of privilege.
304        // What master skips is resolving it into a user; see `crate::request::resolve`.
305        assert_eq!(a.session_token(), Some("r:tok"));
306    }
307
308    #[test]
309    fn a_configured_client_key_becomes_mandatory() {
310        // Easy to trip over: with a client key configured, omitting it fails with a bare 403
311        // that reads like an authorization problem rather than a missing header.
312        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        // It must not be treated as master, and it must not bypass client-key validation.
368        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    /// The installation id travels on every authority, not just on a client request. A master-key
418    /// signup mints a session too, and that session's duplicate destruction reads it.
419    #[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    // -----------------------------------------------------------------------------------------
440    // masterKeyIps
441    // -----------------------------------------------------------------------------------------
442
443    const MASTER: [(&str, &str); 2] = [
444        ("x-parse-application-id", "app"),
445        ("x-parse-master-key", "master"),
446    ];
447
448    /// The shipped defect. 0.2.0 answered `Credentials::Master` here, from any address on a
449    /// server nobody had configured.
450    #[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    /// The control for the default: refusing everything would satisfy the test above.
463    #[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    /// The control for the filter: adding the address makes the identical request succeed, so the
476    /// refusal above is the allowlist and not a broken master key.
477    #[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    /// **A refused master key is refused, not demoted.** Falling through to client-key validation
490    /// would let a caller holding a master key it is not allowed to use skip a check it would
491    /// otherwise have to satisfy, and would answer 200 for a request upstream answers 403.
492    #[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    /// A wrong master key is not a master key at all, so the allowlist never applies to it and the
507    /// request is still an ordinary client request.
508    #[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    /// An ordinary request is not filtered. The allowlist governs the two privileged keys and
523    /// nothing else, so a client from anywhere is still a client.
524    #[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    /// The empty array means the key cannot be used at all, including from the server itself.
538    #[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    /// The maintenance key carries the same filter and the same default. Filtering one privileged
551    /// key and not the other would leave the identical hole one header away.
552    #[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    /// Failing closed when the transport gave no peer address. An allowlist that cannot see the
571    /// address it filters on has not been satisfied, and the alternative reading, "no address
572    /// means no restriction", is the shipped defect written a second time.
573    #[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        // ...and an ordinary client request is unaffected, so an embedder without connect info
593        // still serves everything except the two privileged keys.
594        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    /// **Both keys on one request resolve to maintenance, which is upstream's order.**
602    /// `resolveKeyAuth` tests `maintenanceKeyValue` before the master key, and this was reversed
603    /// here. It was unobservable until the two keys grew separate allowlists, and the tests below
604    /// are the ones that would have caught it.
605    #[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    /// The consequence that makes the order matter, in both directions. Whichever key is tested
621    /// first decides which allowlist applies, so a request carrying both is accepted by one server
622    /// and refused by the other if the two disagree about the order.
623    #[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        // Maintenance refuses this address, master would have allowed it. Upstream refuses.
632        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        // The converse: master refuses, maintenance allows. Upstream accepts, as maintenance.
642        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    /// **The forgery case.** `resolve_with_peer` is given the socket's address and never reads a
653    /// header, so
654    /// there is nothing here for `X-Forwarded-For` to influence. Asserted rather than assumed,
655    /// because "the code does not read the header" is exactly the kind of claim that stays true
656    /// until someone adds proxy support and reads it by default.
657    #[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}