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,
}
pub fn resolve(
config: &ServerConfig,
headers: &http::HeaderMap,
) -> 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) = get(headers::MASTER_KEY) {
if k == config.master_key {
return Ok(with(Credentials::Master));
}
}
if let (Some(k), Some(expected)) = (get(headers::MAINTENANCE_KEY), &config.maintenance_key) {
if k == expected {
return Ok(with(Credentials::Maintenance));
}
}
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))
}
#[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 credentials(
config: &ServerConfig,
pairs: &[(&str, &str)],
) -> Result<Credentials, HeaderRejection> {
resolve(config, &hm(pairs)).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(
&cfg(),
&hm(&[
("x-parse-application-id", "app"),
("x-parse-master-key", "master"),
("x-parse-session-token", "r:tok"),
]),
)
.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(
&cfg(),
&hm(&[
("x-parse-application-id", "app"),
("x-parse-javascript-key", "js"),
("x-parse-session-token", "r:abc"),
]),
)
.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(
&c,
&hm(&[
("x-parse-application-id", "app"),
("x-parse-maintenance-key", "maint"),
]),
)
.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(
&cfg(),
&hm(&[
("x-parse-application-id", "app"),
extra,
("x-parse-installation-id", "inst-1"),
]),
)
.unwrap();
assert_eq!(a.installation_id.as_deref(), Some("inst-1"));
}
}
}