use std::net::IpAddr;
use crate::config::ServerConfig;
pub mod headers {
pub const APP_ID: &str = "x-parse-application-id";
pub const MASTER_KEY: &str = "x-parse-master-key";
pub const MAINTENANCE_KEY: &str = "x-parse-maintenance-key";
pub const JAVASCRIPT_KEY: &str = "x-parse-javascript-key";
pub const REST_API_KEY: &str = "x-parse-rest-api-key";
pub const CLIENT_KEY: &str = "x-parse-client-key";
pub const DOT_NET_KEY: &str = "x-parse-windows-key";
pub const SESSION_TOKEN: &str = "x-parse-session-token";
pub const INSTALLATION_ID: &str = "x-parse-installation-id";
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Credentials {
Master,
Maintenance,
Client,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Authority {
pub credentials: Credentials,
pub session_token: Option<String>,
pub installation_id: Option<String>,
}
impl Authority {
pub fn is_master(&self) -> bool {
matches!(self.credentials, Credentials::Master)
}
pub fn is_privileged(&self) -> bool {
matches!(
self.credentials,
Credentials::Master | Credentials::Maintenance
)
}
pub fn session_token(&self) -> Option<&str> {
self.session_token.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeaderRejection {
Unauthorized,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Peer {
Address(IpAddr),
Unknown,
}
impl Peer {
fn allowed_by(self, allowlist: &crate::ip_allowlist::IpAllowlist) -> bool {
match self {
Peer::Address(ip) => allowlist.allows(ip),
Peer::Unknown => false,
}
}
}
impl From<std::net::SocketAddr> for Peer {
fn from(addr: std::net::SocketAddr) -> Self {
Peer::Address(addr.ip())
}
}
pub fn resolve_with_peer(
config: &ServerConfig,
headers: &http::HeaderMap,
peer: Peer,
) -> Result<Authority, HeaderRejection> {
let get = |name: &str| headers.get(name).and_then(|v| v.to_str().ok());
let installation_id = get(headers::INSTALLATION_ID).map(str::to_string);
let session_token = get(headers::SESSION_TOKEN).map(str::to_string);
let with = |credentials: Credentials| Authority {
credentials,
session_token: session_token.clone(),
installation_id: installation_id.clone(),
};
match get(headers::APP_ID) {
Some(id) if id == config.app_id => {}
_ => return Err(HeaderRejection::Unauthorized),
}
if let (Some(k), Some(expected)) = (get(headers::MAINTENANCE_KEY), &config.maintenance_key) {
if k == expected {
if !peer.allowed_by(&config.maintenance_key_ips) {
return Err(HeaderRejection::Unauthorized);
}
return Ok(with(Credentials::Maintenance));
}
}
if let Some(k) = get(headers::MASTER_KEY) {
if k == config.master_key {
if !peer.allowed_by(&config.master_key_ips) {
return Err(HeaderRejection::Unauthorized);
}
return Ok(with(Credentials::Master));
}
}
if config.requires_client_key() {
let matched = [
(get(headers::JAVASCRIPT_KEY), &config.javascript_key),
(get(headers::REST_API_KEY), &config.rest_api_key),
(get(headers::CLIENT_KEY), &config.client_key),
(get(headers::DOT_NET_KEY), &config.dot_net_key),
]
.iter()
.any(|(presented, expected)| match (presented, expected) {
(Some(p), Some(e)) => p == e,
_ => false,
});
if !matched {
return Err(HeaderRejection::Unauthorized);
}
}
Ok(with(Credentials::Client))
}
#[deprecated(
since = "0.2.1",
note = "the master key is filtered by source address; call resolve_with_peer. \
This form refuses every master and maintenance key because it has no address to check."
)]
pub fn resolve(
config: &ServerConfig,
headers: &http::HeaderMap,
) -> Result<Authority, HeaderRejection> {
resolve_with_peer(config, headers, Peer::Unknown)
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> ServerConfig {
ServerConfig::new("app", "master").javascript_key("js")
}
fn hm(pairs: &[(&str, &str)]) -> http::HeaderMap {
let mut m = http::HeaderMap::new();
for (k, v) in pairs {
m.insert(
http::HeaderName::from_bytes(k.as_bytes()).unwrap(),
http::HeaderValue::from_str(v).unwrap(),
);
}
m
}
fn loopback() -> Peer {
Peer::Address("127.0.0.1".parse().expect("loopback"))
}
fn from(address: &str) -> Peer {
Peer::Address(address.parse().expect("test address"))
}
fn resolve_from(
config: &ServerConfig,
pairs: &[(&str, &str)],
peer: Peer,
) -> Result<Authority, HeaderRejection> {
resolve_with_peer(config, &hm(pairs), peer)
}
fn credentials(
config: &ServerConfig,
pairs: &[(&str, &str)],
) -> Result<Credentials, HeaderRejection> {
resolve_with_peer(config, &hm(pairs), loopback()).map(|a| a.credentials)
}
fn anonymous() -> Credentials {
Credentials::Client
}
#[test]
fn master_key_wins_and_short_circuits_client_key_validation() {
let a = credentials(
&cfg(),
&[
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
],
);
assert_eq!(a, Ok(Credentials::Master));
}
#[test]
fn master_key_beats_a_session_token_on_the_same_request() {
let a = resolve_with_peer(
&cfg(),
&hm(&[
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
("x-parse-session-token", "r:tok"),
]),
loopback(),
)
.unwrap();
assert_eq!(a.credentials, Credentials::Master);
assert!(a.is_master());
assert_eq!(a.session_token(), Some("r:tok"));
}
#[test]
fn a_configured_client_key_becomes_mandatory() {
let missing = credentials(&cfg(), &[("x-parse-application-id", "app")]);
assert_eq!(missing, Err(HeaderRejection::Unauthorized));
let wrong = credentials(
&cfg(),
&[
("x-parse-application-id", "app"),
("x-parse-javascript-key", "nope"),
],
);
assert_eq!(wrong, Err(HeaderRejection::Unauthorized));
let right = credentials(
&cfg(),
&[
("x-parse-application-id", "app"),
("x-parse-javascript-key", "js"),
],
);
assert_eq!(right, Ok(anonymous()));
}
#[test]
fn no_client_key_configured_means_none_required() {
let c = ServerConfig::new("app", "master");
assert_eq!(
credentials(&c, &[("x-parse-application-id", "app")]),
Ok(anonymous())
);
}
#[test]
fn any_one_of_the_configured_keys_suffices() {
let c = ServerConfig::new("app", "master")
.javascript_key("js")
.rest_api_key("rest");
for (k, v) in [
("x-parse-javascript-key", "js"),
("x-parse-rest-api-key", "rest"),
] {
assert!(credentials(&c, &[("x-parse-application-id", "app"), (k, v)]).is_ok());
}
}
#[test]
fn wrong_or_missing_app_id_is_unauthorized() {
assert_eq!(credentials(&cfg(), &[]), Err(HeaderRejection::Unauthorized));
assert_eq!(
credentials(&cfg(), &[("x-parse-application-id", "other")]),
Err(HeaderRejection::Unauthorized)
);
}
#[test]
fn a_wrong_master_key_falls_through_rather_than_short_circuiting() {
let a = credentials(
&cfg(),
&[
("x-parse-application-id", "app"),
("x-parse-master-key", "wrong"),
],
);
assert_eq!(a, Err(HeaderRejection::Unauthorized));
}
#[test]
fn session_token_is_carried_on_client_authority() {
let a = resolve_with_peer(
&cfg(),
&hm(&[
("x-parse-application-id", "app"),
("x-parse-javascript-key", "js"),
("x-parse-session-token", "r:abc"),
]),
loopback(),
)
.unwrap();
assert_eq!(a.session_token(), Some("r:abc"));
}
#[test]
fn maintenance_is_not_master() {
let mut c = ServerConfig::new("app", "master");
c.maintenance_key = Some("maint".into());
let a = resolve_with_peer(
&c,
&hm(&[
("x-parse-application-id", "app"),
("x-parse-maintenance-key", "maint"),
]),
loopback(),
)
.unwrap();
assert_eq!(a.credentials, Credentials::Maintenance);
assert!(
!a.is_master(),
"maintenance must not satisfy a master-key gate"
);
assert!(
a.is_privileged(),
"but it does satisfy the class-security gate"
);
}
#[test]
fn the_installation_id_is_carried_regardless_of_how_the_request_authenticated() {
for extra in [
("x-parse-master-key", "master"),
("x-parse-javascript-key", "js"),
] {
let a = resolve_with_peer(
&cfg(),
&hm(&[
("x-parse-application-id", "app"),
extra,
("x-parse-installation-id", "inst-1"),
]),
loopback(),
)
.unwrap();
assert_eq!(a.installation_id.as_deref(), Some("inst-1"));
}
}
const MASTER: [(&str, &str); 2] = [
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
];
#[test]
fn a_master_key_from_a_non_allowlisted_address_is_refused_at_the_default() {
let c = ServerConfig::new("app", "master");
for peer in ["127.0.0.2", "10.0.0.5", "203.0.113.9", "2001:db8::1"] {
assert_eq!(
resolve_from(&c, &MASTER, from(peer)),
Err(HeaderRejection::Unauthorized),
"{peer} must not be able to use the master key at the default"
);
}
}
#[test]
fn a_master_key_from_loopback_still_works_at_the_default() {
let c = ServerConfig::new("app", "master");
for peer in ["127.0.0.1", "::1", "::ffff:127.0.0.1"] {
assert_eq!(
resolve_from(&c, &MASTER, from(peer)).map(|a| a.credentials),
Ok(Credentials::Master),
"{peer} is the machine the server runs on"
);
}
}
#[test]
fn adding_the_address_admits_the_same_request() {
let mut c = ServerConfig::new("app", "master");
c.master_key_ips =
crate::ip_allowlist::IpAllowlist::parse(["127.0.0.1", "::1", "127.0.0.2"])
.expect("entries");
assert_eq!(
resolve_from(&c, &MASTER, from("127.0.0.2")).map(|a| a.credentials),
Ok(Credentials::Master)
);
}
#[test]
fn a_refused_master_key_does_not_fall_through_to_the_client_key() {
let c = ServerConfig::new("app", "master").javascript_key("js");
let with_client_key = [
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
("x-parse-javascript-key", "js"),
];
assert_eq!(
resolve_from(&c, &with_client_key, from("10.0.0.5")),
Err(HeaderRejection::Unauthorized)
);
}
#[test]
fn a_wrong_master_key_is_unaffected_by_the_allowlist() {
let c = ServerConfig::new("app", "master").javascript_key("js");
let wrong = [
("x-parse-application-id", "app"),
("x-parse-master-key", "nope"),
("x-parse-javascript-key", "js"),
];
assert_eq!(
resolve_from(&c, &wrong, from("10.0.0.5")).map(|a| a.credentials),
Ok(Credentials::Client)
);
}
#[test]
fn the_allowlist_does_not_touch_an_ordinary_client_request() {
let c = ServerConfig::new("app", "master").javascript_key("js");
let client = [
("x-parse-application-id", "app"),
("x-parse-javascript-key", "js"),
];
assert_eq!(
resolve_from(&c, &client, from("203.0.113.9")).map(|a| a.credentials),
Ok(Credentials::Client)
);
}
#[test]
fn an_empty_allowlist_refuses_loopback_too() {
let mut c = ServerConfig::new("app", "master");
c.master_key_ips = crate::ip_allowlist::IpAllowlist::deny_all();
for peer in ["127.0.0.1", "::1"] {
assert_eq!(
resolve_from(&c, &MASTER, from(peer)),
Err(HeaderRejection::Unauthorized)
);
}
}
#[test]
fn the_maintenance_key_is_filtered_the_same_way() {
let mut c = ServerConfig::new("app", "master");
c.maintenance_key = Some("maint".into());
let maint = [
("x-parse-application-id", "app"),
("x-parse-maintenance-key", "maint"),
];
assert_eq!(
resolve_from(&c, &maint, loopback()).map(|a| a.credentials),
Ok(Credentials::Maintenance)
);
assert_eq!(
resolve_from(&c, &maint, from("10.0.0.5")),
Err(HeaderRejection::Unauthorized)
);
}
#[test]
fn an_unknown_peer_cannot_present_a_privileged_key() {
let mut c = ServerConfig::new("app", "master");
c.maintenance_key = Some("maint".into());
assert_eq!(
resolve_from(&c, &MASTER, Peer::Unknown),
Err(HeaderRejection::Unauthorized)
);
assert_eq!(
resolve_from(
&c,
&[
("x-parse-application-id", "app"),
("x-parse-maintenance-key", "maint"),
],
Peer::Unknown,
),
Err(HeaderRejection::Unauthorized)
);
assert_eq!(
resolve_from(&c, &[("x-parse-application-id", "app")], Peer::Unknown)
.map(|a| a.credentials),
Ok(Credentials::Client)
);
}
#[test]
fn both_keys_on_one_request_resolve_to_maintenance() {
let mut c = ServerConfig::new("app", "master");
c.maintenance_key = Some("maint".into());
let both = [
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
("x-parse-maintenance-key", "maint"),
];
assert_eq!(
resolve_from(&c, &both, loopback()).map(|a| a.credentials),
Ok(Credentials::Maintenance)
);
}
#[test]
fn with_both_keys_the_maintenance_allowlist_is_the_one_that_decides() {
let both = [
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
("x-parse-maintenance-key", "maint"),
];
let mut refusing = ServerConfig::new("app", "master");
refusing.maintenance_key = Some("maint".into());
refusing.maintenance_key_ips = crate::ip_allowlist::IpAllowlist::deny_all();
assert_eq!(
resolve_from(&refusing, &both, loopback()),
Err(HeaderRejection::Unauthorized),
"the maintenance allowlist decides, so a master key on the same request cannot rescue it"
);
let mut allowing = ServerConfig::new("app", "master");
allowing.maintenance_key = Some("maint".into());
allowing.master_key_ips = crate::ip_allowlist::IpAllowlist::deny_all();
assert_eq!(
resolve_from(&allowing, &both, loopback()).map(|a| a.credentials),
Ok(Credentials::Maintenance),
"and a refused master key on the same request does not taint an allowed maintenance one"
);
}
#[test]
fn a_forwarded_header_moves_a_caller_neither_in_nor_out() {
let c = ServerConfig::new("app", "master");
let forged = [
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
("x-forwarded-for", "127.0.0.1"),
];
assert_eq!(
resolve_from(&c, &forged, from("10.0.0.5")),
Err(HeaderRejection::Unauthorized),
"a client-supplied header must not admit a non-allowlisted peer"
);
let pointing_out = [
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
("x-forwarded-for", "10.0.0.5"),
];
assert_eq!(
resolve_from(&c, &pointing_out, loopback()).map(|a| a.credentials),
Ok(Credentials::Master),
"and it must not evict an allowlisted one either"
);
}
}