use super::errors::{CedarEntityErrorType, PolicyStoreError};
use super::log_entry::PolicyStoreLogEntry;
use crate::log::Logger;
use crate::log::interface::LogWriter;
use cedar_policy::{Entities, Entity, EntityId, EntityTypeName, EntityUid, Schema};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
#[derive(Debug, Clone)]
pub(super) struct ParsedEntity {
pub entity: Entity,
pub uid: EntityUid,
pub filename: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RawEntityJson {
pub uid: EntityUidJson,
#[serde(default)]
pub attrs: HashMap<String, JsonValue>,
#[serde(default)]
pub parents: Vec<EntityUidJson>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
struct EntityUidJson {
#[serde(rename = "type")]
pub entity_type: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum RawEntitiesWrapper {
Array(Vec<JsonValue>),
Object(HashMap<String, JsonValue>),
}
pub(crate) struct EntityParser;
impl EntityParser {
fn parse_entity(
entity_json: &JsonValue,
filename: &str,
schema: Option<&Schema>,
) -> Result<ParsedEntity, PolicyStoreError> {
let raw_entity: RawEntityJson =
serde_json::from_value(entity_json.clone()).map_err(|e| {
PolicyStoreError::JsonParsing {
file: filename.to_string(),
source: e,
}
})?;
let uid = Self::parse_entity_uid(&raw_entity.uid, filename)?;
for parent_uid in &raw_entity.parents {
Self::parse_entity_uid(parent_uid, filename)?;
}
let entity_json_for_cedar = serde_json::json!({
"uid": {
"type": raw_entity.uid.entity_type,
"id": raw_entity.uid.id
},
"attrs": raw_entity.attrs,
"parents": raw_entity.parents
});
let entity = Entity::from_json_value(entity_json_for_cedar, schema).map_err(|e| {
PolicyStoreError::CedarEntityError {
file: filename.to_string(),
err: CedarEntityErrorType::JsonParseError(format!(
"Failed to parse entity{}: {}",
if schema.is_some() {
" (schema validation failed)"
} else {
""
},
e
)),
}
})?;
Ok(ParsedEntity {
entity,
uid,
filename: filename.to_string(),
content: serde_json::to_string(entity_json).unwrap_or_default(),
})
}
pub(super) fn parse_entities(
content: &str,
filename: &str,
schema: Option<&Schema>,
) -> Result<Vec<ParsedEntity>, PolicyStoreError> {
let json_value: JsonValue =
serde_json::from_str(content).map_err(|e| PolicyStoreError::JsonParsing {
file: filename.to_string(),
source: e,
})?;
let wrapper: RawEntitiesWrapper =
serde_json::from_value(json_value).map_err(|e| PolicyStoreError::CedarEntityError {
file: filename.to_string(),
err: CedarEntityErrorType::JsonParseError(format!(
"Entity file must contain a JSON array or object: {e}"
)),
})?;
let entity_values: Vec<&JsonValue> = match &wrapper {
RawEntitiesWrapper::Array(arr) => arr.iter().collect(),
RawEntitiesWrapper::Object(obj) => obj.values().collect(),
};
let mut parsed_entities = Vec::with_capacity(entity_values.len());
for entity_json in entity_values {
let parsed = Self::parse_entity(entity_json, filename, schema)?;
parsed_entities.push(parsed);
}
Ok(parsed_entities)
}
fn parse_entity_uid(
uid_json: &EntityUidJson,
filename: &str,
) -> Result<EntityUid, PolicyStoreError> {
let entity_type = EntityTypeName::from_str(&uid_json.entity_type).map_err(|e| {
PolicyStoreError::CedarEntityError {
file: filename.to_string(),
err: CedarEntityErrorType::InvalidTypeName(
uid_json.entity_type.clone(),
e.to_string(),
),
}
})?;
let entity_id =
EntityId::from_str(&uid_json.id).map_err(|e| PolicyStoreError::CedarEntityError {
file: filename.to_string(),
err: CedarEntityErrorType::InvalidEntityId(format!(
"Invalid entity ID '{}': {}",
uid_json.id, e
)),
})?;
Ok(EntityUid::from_type_name_and_id(entity_type, entity_id))
}
pub(super) fn detect_duplicates(
entities: Vec<ParsedEntity>,
logger: Option<&Logger>,
) -> HashMap<EntityUid, ParsedEntity> {
let mut entity_map: HashMap<EntityUid, ParsedEntity> =
HashMap::with_capacity(entities.len());
for entity in entities {
if let Some(existing) = entity_map.get(&entity.uid) {
logger.log_any(PolicyStoreLogEntry::warn(format!(
"Duplicate entity UID '{}' found in files '{}' and '{}'. Using the latter.",
entity.uid, existing.filename, entity.filename
)));
}
entity_map.insert(entity.uid.clone(), entity);
}
entity_map
}
pub(super) fn create_entities_store(
entities: Vec<ParsedEntity>,
) -> Result<Entities, PolicyStoreError> {
let entity_list: Vec<Entity> = entities.into_iter().map(|p| p.entity).collect();
Entities::from_entities(entity_list, None).map_err(|e| PolicyStoreError::CedarEntityError {
file: "entity_store".to_string(),
err: CedarEntityErrorType::EntityStoreCreation(e.to_string()),
})
}
pub(super) fn validate_hierarchy(entities: &[ParsedEntity]) -> Result<(), Vec<String>> {
let entity_uids: HashSet<&EntityUid> = entities.iter().map(|e| &e.uid).collect();
let mut errors: Vec<String> = Vec::new();
for parsed_entity in entities {
let parents = &parsed_entity.entity.clone().into_inner().2;
for parent_uid in parents {
if !entity_uids.contains(parent_uid) {
errors.push(format!(
"Entity '{}' in file '{}' references non-existent parent '{}'",
parsed_entity.uid, parsed_entity.filename, parent_uid
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_entity() {
let content = serde_json::json!({
"uid": {
"type": "User",
"id": "alice"
},
"attrs": {
"name": "Alice",
"age": 30
},
"parents": []
});
let result = EntityParser::parse_entity(&content, "user1.json", None);
assert!(
result.is_ok(),
"Should parse simple entity: {:?}",
result.err()
);
let parsed = result.unwrap();
assert_eq!(parsed.filename, "user1.json");
assert_eq!(parsed.uid.to_string(), "User::\"alice\"");
}
#[test]
fn test_parse_entity_with_parents() {
let content = serde_json::json!({
"uid": {
"type": "User",
"id": "bob"
},
"attrs": {
"name": "Bob"
},
"parents": [
{
"type": "Role",
"id": "admin"
},
{
"type": "Role",
"id": "developer"
}
]
});
let parsed = EntityParser::parse_entity(&content, "user2.json", None)
.expect("Should parse entity with parents");
let parents = &parsed.entity.clone().into_inner().2;
assert_eq!(parents.len(), 2, "Should have 2 parents");
}
#[test]
fn test_parse_entity_with_namespace() {
let content = serde_json::json!({
"uid": {
"type": "Jans::User",
"id": "user123"
},
"attrs": {
"email": "user@example.com"
},
"parents": []
});
let parsed = EntityParser::parse_entity(&content, "jans_user.json", None)
.expect("Should parse entity with namespace");
assert_eq!(parsed.uid.to_string(), "Jans::User::\"user123\"");
}
#[test]
fn test_parse_entity_empty_attrs() {
let content = serde_json::json!({
"uid": {
"type": "Resource",
"id": "res1"
},
"attrs": {},
"parents": []
});
EntityParser::parse_entity(&content, "resource.json", None)
.expect("Should parse entity with empty attrs");
}
#[test]
fn test_parse_entity_invalid_json() {
let content = serde_json::json!("not an object");
let result = EntityParser::parse_entity(&content, "invalid.json", None);
let err = result.expect_err("Should fail on invalid JSON");
assert!(
matches!(&err, PolicyStoreError::JsonParsing { file, .. } if file == "invalid.json"),
"Expected JsonParsing error, got: {err:?}"
);
}
#[test]
fn test_parse_entity_invalid_type() {
let content = serde_json::json!({
"uid": {
"type": "Invalid Type Name!",
"id": "test"
},
"attrs": {},
"parents": []
});
let result = EntityParser::parse_entity(&content, "invalid_type.json", None);
let err = result.expect_err("Should fail on invalid entity type");
assert!(
matches!(&err, PolicyStoreError::CedarEntityError { .. }),
"Expected CedarEntityError for invalid entity type, got: {err:?}"
);
}
#[test]
fn test_parse_entities_array() {
let content = r#"[
{
"uid": {"type": "User", "id": "user1"},
"attrs": {"name": "User One"},
"parents": []
},
{
"uid": {"type": "User", "id": "user2"},
"attrs": {"name": "User Two"},
"parents": []
}
]"#;
let parsed = EntityParser::parse_entities(content, "users.json", None)
.expect("Should parse entity array");
assert_eq!(parsed.len(), 2, "Should have 2 entities");
}
#[test]
fn test_parse_entities_object() {
let content = r#"{
"user1": {
"uid": {"type": "User", "id": "user1"},
"attrs": {},
"parents": []
},
"user2": {
"uid": {"type": "User", "id": "user2"},
"attrs": {},
"parents": []
}
}"#;
let parsed = EntityParser::parse_entities(content, "users.json", None)
.expect("Should parse entity object");
assert_eq!(parsed.len(), 2, "Should have 2 entities");
}
#[test]
fn test_detect_duplicates_none() {
let entities = vec![
ParsedEntity {
entity: Entity::new(
"User::\"alice\"".parse().unwrap(),
HashMap::new(),
HashSet::new(),
)
.unwrap(),
uid: "User::\"alice\"".parse().unwrap(),
filename: "user1.json".to_string(),
content: String::new(),
},
ParsedEntity {
entity: Entity::new(
"User::\"bob\"".parse().unwrap(),
HashMap::new(),
HashSet::new(),
)
.unwrap(),
uid: "User::\"bob\"".parse().unwrap(),
filename: "user2.json".to_string(),
content: String::new(),
},
];
let map = EntityParser::detect_duplicates(entities, None);
assert_eq!(map.len(), 2, "Should have 2 unique entities");
}
#[test]
fn test_detect_duplicates_uses_latest() {
let entities = vec![
ParsedEntity {
entity: Entity::new(
"User::\"alice\"".parse().unwrap(),
HashMap::new(),
HashSet::new(),
)
.unwrap(),
uid: "User::\"alice\"".parse().unwrap(),
filename: "user1.json".to_string(),
content: String::new(),
},
ParsedEntity {
entity: Entity::new(
"User::\"alice\"".parse().unwrap(),
HashMap::new(),
HashSet::new(),
)
.unwrap(),
uid: "User::\"alice\"".parse().unwrap(),
filename: "user2.json".to_string(),
content: String::new(),
},
];
let map = EntityParser::detect_duplicates(entities, None);
assert_eq!(
map.len(),
1,
"Should have 1 unique entity after handling duplicate"
);
let alice = map.get(&"User::\"alice\"".parse().unwrap()).unwrap();
assert_eq!(
alice.filename, "user2.json",
"Should use the latest entity (last-write-wins)"
);
}
#[test]
fn test_validate_hierarchy_valid() {
let parent = ParsedEntity {
entity: Entity::new(
"Role::\"admin\"".parse().unwrap(),
HashMap::new(),
HashSet::new(),
)
.unwrap(),
uid: "Role::\"admin\"".parse().unwrap(),
filename: "role.json".to_string(),
content: String::new(),
};
let mut parent_set = HashSet::new();
parent_set.insert("Role::\"admin\"".parse().unwrap());
let child = ParsedEntity {
entity: Entity::new(
"User::\"alice\"".parse().unwrap(),
HashMap::new(),
parent_set,
)
.unwrap(),
uid: "User::\"alice\"".parse().unwrap(),
filename: "user.json".to_string(),
content: String::new(),
};
let entities = vec![parent, child];
EntityParser::validate_hierarchy(&entities).expect("Hierarchy should be valid");
}
#[test]
fn test_validate_hierarchy_missing_parent() {
let mut parent_set = HashSet::new();
parent_set.insert("Role::\"admin\"".parse().unwrap());
let child = ParsedEntity {
entity: Entity::new(
"User::\"alice\"".parse().unwrap(),
HashMap::new(),
parent_set,
)
.unwrap(),
uid: "User::\"alice\"".parse().unwrap(),
filename: "user.json".to_string(),
content: String::new(),
};
let entities = vec![child];
let result = EntityParser::validate_hierarchy(&entities);
let errors = result.expect_err("Should detect missing parent");
assert_eq!(errors.len(), 1, "Should have 1 hierarchy error");
assert!(
errors[0].contains("Role::\"admin\""),
"Error should reference missing parent Role::admin, got: {}",
errors[0]
);
}
#[test]
fn test_create_entities_store() {
let entities = vec![
ParsedEntity {
entity: Entity::new(
"User::\"alice\"".parse().unwrap(),
HashMap::new(),
HashSet::new(),
)
.unwrap(),
uid: "User::\"alice\"".parse().unwrap(),
filename: "user1.json".to_string(),
content: String::new(),
},
ParsedEntity {
entity: Entity::new(
"User::\"bob\"".parse().unwrap(),
HashMap::new(),
HashSet::new(),
)
.unwrap(),
uid: "User::\"bob\"".parse().unwrap(),
filename: "user2.json".to_string(),
content: String::new(),
},
];
let store =
EntityParser::create_entities_store(entities).expect("Should create entity store");
assert_eq!(store.iter().count(), 2, "Store should have 2 entities");
}
#[test]
fn test_parse_entity_with_schema_validation() {
use cedar_policy::{Schema, SchemaFragment};
use std::str::FromStr;
let schema_src = r"
entity User = {
name: String,
age: Long
};
";
let fragment = SchemaFragment::from_str(schema_src).expect("Should parse schema");
let schema = Schema::from_schema_fragments([fragment]).expect("Should create schema");
let valid_content = serde_json::json!({
"uid": {
"type": "User",
"id": "alice"
},
"attrs": {
"name": "Alice",
"age": 30
},
"parents": []
});
let result = EntityParser::parse_entity(&valid_content, "user.json", Some(&schema));
assert!(
result.is_ok(),
"Should parse entity with valid schema: {:?}",
result.err()
);
}
}