use indexmap::IndexMap;
use parse_rust_auth::{expand_roles, resolve_session, RolePrincipal};
use parse_rust_core::{ClassLevelPermissions, ParseError, ParseMap, ParseValue};
use parse_rust_mongo::MongoAdapter;
use parse_rust_rest::{AclScope, Ctx, PermissionOptions, SchemaSnapshot};
use parse_rust_storage::{ClassSchema, StorageAdapter};
use crate::auth::{Authority, Credentials};
use crate::config::ServerConfig;
pub struct RequestContext {
pub snapshot: SchemaSnapshot,
pub scope: AclScope,
pub options: PermissionOptions,
pub session_token: Option<String>,
pub user_id: Option<String>,
pub installation_id: Option<String>,
pub save_response_exempt: bool,
pub is_maintenance: bool,
}
impl RequestContext {
pub fn ctx<'a>(&'a self, storage: &'a MongoAdapter) -> Ctx<'a, MongoAdapter> {
Ctx::new(storage, &self.snapshot, &self.scope, &self.options)
.maintenance(self.is_maintenance)
}
pub fn is_master(&self) -> bool {
self.scope.is_master()
}
}
pub async fn resolve(
storage: &MongoAdapter,
config: &ServerConfig,
authority: &Authority,
) -> Result<RequestContext, ParseError> {
let (scope, user_id) = match (&authority.credentials, authority.session_token.as_deref()) {
(Credentials::Master | Credentials::Maintenance, _) => (AclScope::Unrestricted, None),
(Credentials::Client, None) => (AclScope::Anonymous, None),
(Credentials::Client, Some(token)) => {
let session = resolve_session(storage, token).await?;
let roles = expand_roles(storage, RolePrincipal::User(&session.user_object_id)).await?;
let names = roles.iter().map(|r| r.as_str().to_string()).collect();
let scope = AclScope::user(session.user_object_id.clone(), names)?;
(scope, Some(session.user_object_id))
}
};
let mut classes = storage.all_schemas().await?;
merge_server_protected_fields(&mut classes, config);
let snapshot = SchemaSnapshot::from_classes(classes);
Ok(RequestContext {
snapshot,
scope,
options: config.permission_options(),
session_token: authority.session_token.clone(),
user_id,
installation_id: authority.installation_id.clone(),
save_response_exempt: config.protected_fields_save_response_exempt,
is_maintenance: matches!(authority.credentials, Credentials::Maintenance),
})
}
fn merge_server_protected_fields(classes: &mut [ClassSchema], config: &ServerConfig) {
if config.protected_fields.is_empty() {
return;
}
for schema in classes.iter_mut() {
let Some(configured) = config.protected_fields.get(&schema.class_name) else {
continue;
};
let raw = schema
.clp
.as_ref()
.map(|clp| clp.raw().clone())
.unwrap_or_default();
schema.clp = Some(ClassLevelPermissions::from_map(union_protected_fields(
raw, configured,
)));
}
}
fn union_protected_fields(
mut raw: ParseMap,
configured: &IndexMap<String, Vec<String>>,
) -> ParseMap {
let mut protected = match raw.shift_remove("protectedFields") {
Some(ParseValue::Object(map)) => map,
_ => ParseMap::new(),
};
for (entity, fields) in configured {
let mut merged: Vec<String> = match protected.get(entity) {
Some(ParseValue::Array(items)) => items
.iter()
.filter_map(|v| match v {
ParseValue::String(s) => Some(s.clone()),
_ => None,
})
.collect(),
_ => Vec::new(),
};
for field in fields {
if !merged.contains(field) {
merged.push(field.clone());
}
}
protected.insert(
entity.clone(),
ParseValue::Array(merged.into_iter().map(ParseValue::String).collect()),
);
}
raw.insert("protectedFields".to_string(), ParseValue::Object(protected));
raw
}
#[cfg(test)]
mod tests {
use super::*;
use parse_rust_core::PfEntity;
fn configured(pairs: &[(&str, &[&str])]) -> IndexMap<String, Vec<String>> {
pairs
.iter()
.map(|(k, v)| {
(
(*k).to_string(),
v.iter().map(|s| (*s).to_string()).collect(),
)
})
.collect()
}
fn parse(json: &str) -> ParseMap {
match parse_rust_core::classify(serde_json::from_str(json).expect("test literal"))
.expect("classify")
{
ParseValue::Object(m) => m,
_ => panic!("expected an object"),
}
}
#[test]
fn the_server_option_is_unioned_into_the_class_block() {
let raw = parse(r#"{"find":{"*":true},"protectedFields":{"*":["phone"]}}"#);
let merged = ClassLevelPermissions::from_map(union_protected_fields(
raw,
&configured(&[("*", &["email"])]),
));
assert_eq!(
merged.protected_fields().get(&PfEntity::Public),
Some(&vec!["phone".to_string(), "email".to_string()]),
"the stored list keeps its order and the configured entry is appended"
);
assert!(merged.raw().contains_key("find"));
}
#[test]
fn a_class_with_no_block_gets_one_carrying_only_protected_fields() {
let merged = ClassLevelPermissions::from_map(union_protected_fields(
ParseMap::new(),
&configured(&[("*", &["email"])]),
));
assert_eq!(merged.raw().len(), 1, "no operation entry is invented");
assert_eq!(
merged.protected_fields().get(&PfEntity::Public),
Some(&vec!["email".to_string()])
);
for op in parse_rust_core::Operation::ALL {
assert!(
merged.op(op).is_none(),
"an absent operation entry stays absent, which is unrestricted"
);
}
}
#[test]
fn a_field_already_protected_is_not_duplicated() {
let raw = parse(r#"{"protectedFields":{"*":["email"]}}"#);
let merged = ClassLevelPermissions::from_map(union_protected_fields(
raw,
&configured(&[("*", &["email"])]),
));
assert_eq!(
merged.protected_fields().get(&PfEntity::Public),
Some(&vec!["email".to_string()])
);
}
}