use indexmap::IndexMap;
use parse_rust_auth::SessionConfig;
use parse_rust_core::ErrorDetail;
use parse_rust_rest::PermissionOptions;
use parse_rust_schema::{ClpValidation, ObjectIdForm, Unenforceable};
pub const REPORTED_PARSE_SERVER_VERSION: &str = "9.10.1-alpha.6";
#[derive(Debug, Clone)]
pub struct FeatureSupport {
pub global_config: bool,
pub hooks: bool,
pub cloud_code_jobs: bool,
pub logs: bool,
pub schemas: bool,
pub push_audiences: bool,
}
impl Default for FeatureSupport {
fn default() -> Self {
Self {
global_config: false,
hooks: false,
cloud_code_jobs: false,
logs: false,
schemas: true,
push_audiences: false,
}
}
}
pub type ProtectedFieldsConfig = IndexMap<String, IndexMap<String, Vec<String>>>;
pub fn default_protected_fields() -> ProtectedFieldsConfig {
let mut entities = IndexMap::new();
entities.insert("*".to_string(), vec!["email".to_string()]);
let mut classes = ProtectedFieldsConfig::new();
classes.insert("_User".to_string(), entities);
classes
}
pub fn merge_protected_fields_defaults(configured: &mut ProtectedFieldsConfig, owner_exempt: bool) {
for (class_name, default_entities) in default_protected_fields() {
let Some(entities) = configured.get_mut(&class_name) else {
configured.insert(class_name, default_entities);
continue;
};
for (entity, default_fields) in default_entities {
match entities.get_mut(&entity) {
Some(_) if !owner_exempt => {}
Some(fields) => {
for field in default_fields {
if !fields.contains(&field) {
fields.push(field);
}
}
}
None => {
entities.insert(entity, default_fields);
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct ServerConfig {
pub app_id: String,
pub master_key: String,
pub maintenance_key: Option<String>,
pub javascript_key: Option<String>,
pub rest_api_key: Option<String>,
pub client_key: Option<String>,
pub dot_net_key: Option<String>,
pub mount_path: String,
pub enable_sanitized_error_response: bool,
pub has_push_support: bool,
pub has_push_scheduled_support: bool,
pub security_check_enabled: bool,
pub features: FeatureSupport,
pub session: SessionConfig,
pub protected_fields: ProtectedFieldsConfig,
pub protected_fields_owner_exempt: bool,
pub protected_fields_save_response_exempt: bool,
pub allow_custom_object_id: bool,
pub allow_client_class_creation: bool,
pub allow_origin: Vec<String>,
pub allow_headers: Vec<String>,
pub batch_request_limit: i64,
pub create_index_role_name: bool,
}
impl ServerConfig {
pub fn new(app_id: impl Into<String>, master_key: impl Into<String>) -> Self {
Self {
app_id: app_id.into(),
master_key: master_key.into(),
maintenance_key: None,
javascript_key: None,
rest_api_key: None,
client_key: None,
dot_net_key: None,
mount_path: "/parse".to_string(),
enable_sanitized_error_response: true,
has_push_support: false,
has_push_scheduled_support: false,
security_check_enabled: false,
features: FeatureSupport::default(),
session: SessionConfig::default(),
protected_fields: default_protected_fields(),
protected_fields_owner_exempt: true,
protected_fields_save_response_exempt: true,
allow_custom_object_id: false,
allow_client_class_creation: false,
allow_origin: vec!["*".to_string()],
allow_headers: Vec::new(),
batch_request_limit: -1,
create_index_role_name: true,
}
}
pub fn error_detail(&self) -> ErrorDetail {
ErrorDetail::from_sanitized(self.enable_sanitized_error_response)
}
pub fn object_id_form(&self) -> ObjectIdForm {
if self.allow_custom_object_id {
ObjectIdForm::Custom
} else {
ObjectIdForm::Generated
}
}
pub fn clp_validation(&self) -> ClpValidation {
ClpValidation {
object_id: self.object_id_form(),
unenforceable: Unenforceable::Accept,
}
}
pub fn permission_options(&self) -> PermissionOptions {
PermissionOptions {
protected_fields_owner_exempt: self.protected_fields_owner_exempt,
error_detail: self.error_detail(),
allow_client_class_creation: self.allow_client_class_creation,
}
}
pub fn javascript_key(mut self, k: impl Into<String>) -> Self {
self.javascript_key = Some(k.into());
self
}
pub fn rest_api_key(mut self, k: impl Into<String>) -> Self {
self.rest_api_key = Some(k.into());
self
}
pub fn mount_path(mut self, p: impl Into<String>) -> Self {
self.mount_path = p.into();
self
}
pub fn requires_client_key(&self) -> bool {
self.javascript_key.is_some()
|| self.rest_api_key.is_some()
|| self.client_key.is_some()
|| self.dot_net_key.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fields(config: &ProtectedFieldsConfig, class: &str, entity: &str) -> Vec<String> {
config
.get(class)
.and_then(|e| e.get(entity))
.cloned()
.unwrap_or_default()
}
#[test]
fn configuring_an_unrelated_class_still_protects_user_email() {
let mut configured = ProtectedFieldsConfig::new();
let mut post = IndexMap::new();
post.insert("*".to_string(), vec!["secret".to_string()]);
configured.insert("Post".to_string(), post);
merge_protected_fields_defaults(&mut configured, true);
assert_eq!(fields(&configured, "_User", "*"), vec!["email".to_string()]);
assert_eq!(fields(&configured, "Post", "*"), vec!["secret".to_string()]);
}
#[test]
fn a_user_block_for_another_entity_gains_the_default_star() {
let mut configured = ProtectedFieldsConfig::new();
let mut user = IndexMap::new();
user.insert("authenticated".to_string(), vec!["phone".to_string()]);
configured.insert("_User".to_string(), user);
merge_protected_fields_defaults(&mut configured, true);
assert_eq!(fields(&configured, "_User", "*"), vec!["email".to_string()]);
assert_eq!(
fields(&configured, "_User", "authenticated"),
vec!["phone".to_string()]
);
}
#[test]
fn the_same_entity_key_is_unioned_without_duplicating() {
let mut configured = ProtectedFieldsConfig::new();
let mut user = IndexMap::new();
user.insert(
"*".to_string(),
vec!["phone".to_string(), "email".to_string()],
);
configured.insert("_User".to_string(), user);
merge_protected_fields_defaults(&mut configured, true);
assert_eq!(
fields(&configured, "_User", "*"),
vec!["phone".to_string(), "email".to_string()]
);
}
#[test]
fn owner_exempt_false_leaves_a_configured_entity_key_alone() {
let mut configured = ProtectedFieldsConfig::new();
let mut user = IndexMap::new();
user.insert("*".to_string(), vec!["phone".to_string()]);
configured.insert("_User".to_string(), user);
merge_protected_fields_defaults(&mut configured, false);
assert_eq!(fields(&configured, "_User", "*"), vec!["phone".to_string()]);
}
#[test]
fn owner_exempt_false_still_fills_in_an_absent_class() {
let mut configured = ProtectedFieldsConfig::new();
let mut post = IndexMap::new();
post.insert("*".to_string(), vec!["secret".to_string()]);
configured.insert("Post".to_string(), post);
merge_protected_fields_defaults(&mut configured, false);
assert_eq!(fields(&configured, "_User", "*"), vec!["email".to_string()]);
}
}