use super::entity_parser::EntityParser;
use super::issuer_parser::IssuerParser;
use super::loader::LoadedPolicyStore;
use super::log_entry::PolicyStoreLogEntry;
use super::policy_parser::PolicyParser;
use super::schema_parser::ParsedSchema;
use super::{PoliciesContainer, PolicyStore, TrustedIssuer};
use crate::common::cedar_schema::CedarSchema;
use crate::common::cedar_schema::cedar_json::CedarSchemaJson;
use crate::common::default_entities::parse_default_entities_with_warns;
use crate::log::Logger;
use crate::log::interface::LogWriter;
use cedar_policy::PolicySet;
use cedar_policy_core::extensions::Extensions;
use cedar_policy_core::validator::ValidatorSchema;
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum ConversionError {
#[error("Failed to convert schema: {0}")]
SchemaConversion(String),
#[error("Failed to convert policies: {0}")]
PolicyConversion(String),
#[error("Failed to convert trusted issuers: {0}")]
IssuerConversion(String),
#[error("Failed to convert entities: {0}")]
EntityConversion(String),
#[error("Failed to create policy set: {0}")]
PolicySetCreation(String),
}
pub(crate) struct PolicyStoreManager;
impl PolicyStoreManager {
pub(crate) fn convert_to_legacy(
loaded: LoadedPolicyStore,
strict_schema_validation: bool,
) -> Result<PolicyStore, ConversionError> {
let cedar_schema = match loaded.schema {
Some(ref parsed_schema) => {
Some(Self::convert_parsed_schema(parsed_schema)?)
},
None if strict_schema_validation => {
return Err(ConversionError::SchemaConversion(
"missing required schema in policy store".to_string(),
));
},
None => None,
};
let policies_container =
Self::convert_policies_and_templates(&loaded.policies, &loaded.templates)?;
let trusted_issuers = Self::convert_trusted_issuers(&loaded.trusted_issuers)?;
let raw_entities = Self::convert_entities(&loaded.entities, None)?;
let default_entities = parse_default_entities_with_warns(raw_entities).map_err(|e| {
ConversionError::EntityConversion(format!("Failed to parse default entities: {e}"))
})?;
Ok(PolicyStore {
version: Some(loaded.metadata.policy_store.version),
schema: cedar_schema,
schema_source_exists: loaded.schema_source_exists,
policies: policies_container,
trusted_issuers,
default_entities,
})
}
fn convert_parsed_schema(parsed: &ParsedSchema) -> Result<CedarSchema, ConversionError> {
let schema = parsed.get_schema().clone();
let json_string = parsed.get_fragment().to_json_string().map_err(|e| {
ConversionError::SchemaConversion(format!("Failed to serialize schema to JSON: {e}"))
})?;
let json: CedarSchemaJson = serde_json::from_str(&json_string).map_err(|e| {
ConversionError::SchemaConversion(format!("Failed to parse CedarSchemaJson: {e}"))
})?;
let validator_schema = ValidatorSchema::from_json_str(
&json_string,
Extensions::all_available(),
)
.map_err(|e| {
ConversionError::SchemaConversion(format!("Failed to create ValidatorSchema: {e}"))
})?;
Ok(CedarSchema {
schema,
json,
validator_schema,
})
}
#[cfg(test)]
fn convert_schema(schema_content: &str) -> Result<CedarSchema, ConversionError> {
use super::schema_parser::ParsedSchema;
let parsed_schema =
ParsedSchema::parse(schema_content, "schema.cedarschema").map_err(|e| {
ConversionError::SchemaConversion(format!("Failed to parse schema: {e}"))
})?;
Self::convert_parsed_schema(&parsed_schema)
}
fn convert_policies_and_templates(
policy_files: &[super::loader::PolicyFile],
template_files: &[super::loader::PolicyFile],
) -> Result<PoliciesContainer, ConversionError> {
if policy_files.is_empty() && template_files.is_empty() {
let policy_set = PolicySet::new();
return Ok(PoliciesContainer::new_empty(policy_set));
}
let mut parsed_policies = Vec::new();
for file in policy_files {
let parsed_list =
PolicyParser::parse_policy(&file.content, &file.name).map_err(|e| {
ConversionError::PolicyConversion(format!(
"Failed to parse '{}': {}",
file.name, e
))
})?;
parsed_policies.extend(parsed_list);
}
let mut parsed_templates = Vec::new();
for file in template_files {
let parsed_list =
PolicyParser::parse_template(&file.content, &file.name).map_err(|e| {
ConversionError::PolicyConversion(format!(
"Failed to parse template '{}': {}",
file.name, e
))
})?;
parsed_templates.extend(parsed_list);
}
let policy_set =
PolicyParser::create_policy_set(parsed_policies.clone(), parsed_templates.clone())
.map_err(|e| ConversionError::PolicySetCreation(e.to_string()))?;
let raw_policy_info = parsed_policies
.into_iter()
.map(|p| (p.id.to_string(), format!("Policy from {}", p.filename)))
.collect();
Ok(PoliciesContainer::new(policy_set, raw_policy_info))
}
fn convert_trusted_issuers(
issuer_files: &[super::loader::IssuerFile],
) -> Result<Option<HashMap<String, TrustedIssuer>>, ConversionError> {
if issuer_files.is_empty() {
return Ok(None);
}
let mut all_issuers = Vec::new();
for file in issuer_files {
let parsed = IssuerParser::parse_issuer(&file.content, &file.name).map_err(|e| {
ConversionError::IssuerConversion(format!("Failed to parse '{}': {}", file.name, e))
})?;
all_issuers.extend(parsed);
}
if let Err(errors) = IssuerParser::validate_issuers(&all_issuers) {
let error_details = errors
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join("; ");
return Err(ConversionError::IssuerConversion(error_details));
}
let issuer_map = IssuerParser::create_issuer_map(all_issuers);
Ok(Some(issuer_map))
}
fn convert_entities(
entity_files: &[super::loader::EntityFile],
logger: Option<&Logger>,
) -> Result<Option<HashMap<String, serde_json::Value>>, ConversionError> {
if entity_files.is_empty() {
return Ok(None);
}
let mut all_parsed_entities = Vec::new();
for file in entity_files {
let parsed =
EntityParser::parse_entities(&file.content, &file.name, None).map_err(|e| {
ConversionError::EntityConversion(format!(
"Failed to parse '{}': {}",
file.name, e
))
})?;
all_parsed_entities.extend(parsed);
}
let unique_entities = EntityParser::detect_duplicates(all_parsed_entities.clone(), logger);
if let Err(warnings) = EntityParser::validate_hierarchy(&all_parsed_entities) {
logger.log_any(PolicyStoreLogEntry::warn(format!(
"Entity hierarchy validation warnings (non-fatal): {warnings:?}"
)));
}
EntityParser::create_entities_store(all_parsed_entities).map_err(|e| {
ConversionError::EntityConversion(format!("Failed to create entity store: {e}"))
})?;
let mut result = HashMap::with_capacity(unique_entities.len());
for (uid, parsed_entity) in unique_entities {
let json_value = parsed_entity.entity.to_json_value().map_err(|e| {
ConversionError::EntityConversion(format!(
"Failed to serialize entity '{}' from '{}': {}. Original content: {}",
uid,
parsed_entity.filename,
e,
if parsed_entity.content.len() > 200 {
format!("{}...(truncated)", &parsed_entity.content[..200])
} else {
parsed_entity.content.clone()
}
))
})?;
result.insert(uid.to_string(), json_value);
}
Ok(Some(result))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::policy_store::loader::{EntityFile, IssuerFile, PolicyFile};
use crate::common::policy_store::metadata::{PolicyStoreInfo, PolicyStoreMetadata};
fn create_test_metadata() -> PolicyStoreMetadata {
PolicyStoreMetadata {
cedar_version: "4.0.0".to_string(),
policy_store: PolicyStoreInfo {
id: "test123".to_string(),
name: "Test Store".to_string(),
description: Some("A test policy store".to_string()),
version: "1.0.0".to_string(),
created_date: None,
updated_date: None,
},
}
}
fn parse_schema(content: &str) -> ParsedSchema {
ParsedSchema::parse(content, "schema.cedarschema").expect("test schema should parse")
}
#[test]
fn test_convert_schema_valid() {
let schema_content = r#"
namespace TestApp {
entity User;
entity Resource;
action "read" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#;
let result = PolicyStoreManager::convert_schema(schema_content);
assert!(
result.is_ok(),
"Schema conversion failed: {:?}",
result.err()
);
let cedar_schema = result.unwrap();
let entity_types: Vec<_> = cedar_schema.schema.entity_types().collect();
assert!(!entity_types.is_empty());
}
#[test]
fn test_convert_schema_invalid() {
let schema_content = "this is not valid cedar schema syntax {{{";
let result = PolicyStoreManager::convert_schema(schema_content);
let err = result.expect_err("Expected error for invalid Cedar schema syntax");
assert!(
matches!(err, ConversionError::SchemaConversion(_)),
"Expected SchemaConversion error, got: {err:?}"
);
}
#[test]
fn test_convert_policies_valid() {
let policy_files = vec![
PolicyFile {
name: "allow.cedar".to_string(),
content: "permit(principal, action, resource);".to_string(),
},
PolicyFile {
name: "deny.cedar".to_string(),
content: "forbid(principal, action, resource);".to_string(),
},
];
let template_files: Vec<PolicyFile> = vec![];
let result =
PolicyStoreManager::convert_policies_and_templates(&policy_files, &template_files);
assert!(
result.is_ok(),
"Policy conversion failed: {:?}",
result.err()
);
let container = result.unwrap();
assert!(!container.get_set().is_empty());
}
#[test]
fn test_convert_policies_with_templates() {
let policy_files = vec![PolicyFile {
name: "allow.cedar".to_string(),
content: "permit(principal, action, resource);".to_string(),
}];
let template_files = vec![PolicyFile {
name: "template.cedar".to_string(),
content: "permit(principal == ?principal, action, resource);".to_string(),
}];
let result =
PolicyStoreManager::convert_policies_and_templates(&policy_files, &template_files);
assert!(
result.is_ok(),
"Policy/template conversion failed: {:?}",
result.err()
);
let container = result.unwrap();
assert!(!container.get_set().is_empty());
}
#[test]
fn test_convert_with_multi_policy_and_multi_template_files() {
let policy_files = vec![PolicyFile {
name: "combined.cedar".to_string(),
content: concat!(
"@id(\"p1\") permit(principal, action, resource);\n",
"@id(\"p2\") forbid(principal, action, resource);\n",
)
.to_string(),
}];
let template_files = vec![PolicyFile {
name: "tpls.cedar".to_string(),
content: concat!(
"@id(\"t1\") permit(principal == ?principal, action, resource);\n",
"@id(\"t2\") permit(principal, action, resource == ?resource);\n",
)
.to_string(),
}];
let container =
PolicyStoreManager::convert_policies_and_templates(&policy_files, &template_files)
.expect("multi-policy + multi-template conversion should succeed");
let set = container.get_set();
let policy_ids: Vec<String> = set.policies().map(|p| p.id().to_string()).collect();
let template_ids: Vec<String> = set.templates().map(|t| t.id().to_string()).collect();
assert!(
policy_ids.contains(&"p1".to_string()) && policy_ids.contains(&"p2".to_string()),
"expected both policy ids p1 and p2, got {policy_ids:?}"
);
assert!(
template_ids.contains(&"t1".to_string()) && template_ids.contains(&"t2".to_string()),
"expected both template ids t1 and t2, got {template_ids:?}"
);
}
#[test]
fn test_convert_policies_empty() {
let policy_files: Vec<PolicyFile> = vec![];
let template_files: Vec<PolicyFile> = vec![];
let result =
PolicyStoreManager::convert_policies_and_templates(&policy_files, &template_files);
assert!(result.is_ok());
let container = result.unwrap();
assert!(container.get_set().is_empty());
}
#[test]
fn test_convert_policies_invalid() {
let policy_files = vec![PolicyFile {
name: "invalid.cedar".to_string(),
content: "this is not valid cedar policy".to_string(),
}];
let template_files: Vec<PolicyFile> = vec![];
let result =
PolicyStoreManager::convert_policies_and_templates(&policy_files, &template_files);
let err = result.expect_err("Expected ConversionError for invalid policy syntax");
assert!(
matches!(err, ConversionError::PolicyConversion(_)),
"Expected PolicyConversion error, got: {err:?}"
);
}
#[test]
fn test_convert_trusted_issuers_valid() {
let issuer_files = vec![IssuerFile {
name: "issuer.json".to_string(),
content: r#"{
"id": "test_issuer",
"name": "Test Issuer",
"description": "A test issuer",
"configuration_endpoint": "https://test.com/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"entity_type_name": "Test::access_token"
}
}
}"#
.to_string(),
}];
let result = PolicyStoreManager::convert_trusted_issuers(&issuer_files);
assert!(
result.is_ok(),
"Issuer conversion failed: {:?}",
result.err()
);
let issuers = result.unwrap();
assert!(issuers.is_some());
let issuers = issuers.unwrap();
assert_eq!(issuers.len(), 1);
assert!(issuers.contains_key("test_issuer"));
}
#[test]
fn test_convert_trusted_issuers_empty() {
let issuer_files: Vec<IssuerFile> = vec![];
let result = PolicyStoreManager::convert_trusted_issuers(&issuer_files);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_convert_entities_valid() {
let entity_files = vec![EntityFile {
name: "users.json".to_string(),
content: r#"[
{
"uid": {"type": "User", "id": "alice"},
"attrs": {"name": "Alice"},
"parents": []
}
]"#
.to_string(),
}];
let result = PolicyStoreManager::convert_entities(&entity_files, None);
assert!(
result.is_ok(),
"Entity conversion failed: {:?}",
result.err()
);
let entities = result.unwrap();
assert!(entities.is_some());
let entities = entities.unwrap();
assert_eq!(entities.len(), 1);
}
#[test]
fn test_convert_entities_empty() {
let entity_files: Vec<EntityFile> = vec![];
let result = PolicyStoreManager::convert_entities(&entity_files, None);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_convert_to_legacy_without_schema_strict_true_errors() {
let loaded = LoadedPolicyStore {
metadata: create_test_metadata(),
schema: None,
schema_source_exists: false,
policies: vec![PolicyFile {
name: "test.cedar".to_string(),
content: "permit(principal, action, resource);".to_string(),
}],
templates: vec![],
entities: vec![],
trusted_issuers: vec![],
};
let result = PolicyStoreManager::convert_to_legacy(loaded, true);
let err = result.expect_err(
"Expected error when strict_schema_validation is true but schema is missing",
);
assert!(
matches!(&err, ConversionError::SchemaConversion(msg) if msg.contains("missing required schema")),
"Expected SchemaConversion error about missing schema, got: {err:?}"
);
}
#[test]
fn test_convert_to_legacy_without_schema_strict_false_succeeds() {
let loaded = LoadedPolicyStore {
metadata: create_test_metadata(),
schema: None,
schema_source_exists: false,
policies: vec![PolicyFile {
name: "test.cedar".to_string(),
content: "permit(principal, action, resource);".to_string(),
}],
templates: vec![],
entities: vec![],
trusted_issuers: vec![],
};
let result = PolicyStoreManager::convert_to_legacy(loaded, false);
let store = result.expect(
"Should succeed when strict_schema_validation is false even without schema",
);
assert!(
store.schema.is_none(),
"schema should be None when no schema was loaded"
);
}
#[test]
fn test_convert_to_legacy_with_schema_strict_false_succeeds() {
let schema = parse_schema(r#"
namespace TestApp {
entity User;
action "read" appliesTo {
principal: [User],
resource: [User]
};
}
"#);
let loaded = LoadedPolicyStore {
metadata: create_test_metadata(),
schema: Some(schema),
schema_source_exists: true,
policies: vec![PolicyFile {
name: "test.cedar".to_string(),
content: "permit(principal, action, resource);".to_string(),
}],
templates: vec![],
entities: vec![],
trusted_issuers: vec![],
};
let result = PolicyStoreManager::convert_to_legacy(loaded, false);
let store = result.expect(
"Should succeed with schema even when strict_schema_validation is false",
);
assert!(
store.schema.is_some(),
"schema should be Some when schema content was provided"
);
}
#[test]
fn test_convert_to_legacy_minimal() {
let schema = parse_schema(r#"
namespace TestApp {
entity User;
action "read" appliesTo {
principal: [User],
resource: [User]
};
}
"#);
let loaded = LoadedPolicyStore {
metadata: create_test_metadata(),
schema: Some(schema),
schema_source_exists: true,
policies: vec![PolicyFile {
name: "test.cedar".to_string(),
content: "permit(principal, action, resource);".to_string(),
}],
templates: vec![],
entities: vec![],
trusted_issuers: vec![],
};
let result = PolicyStoreManager::convert_to_legacy(loaded, true);
assert!(result.is_ok(), "Conversion failed: {:?}", result.err());
let store = result.unwrap();
assert_eq!(store.version, Some("1.0.0".to_string()));
assert!(!store.policies.get_set().is_empty());
assert!(store.trusted_issuers.is_none());
assert_eq!(store.default_entities.entities().len(), 0);
}
#[test]
fn test_convert_to_legacy_full() {
let schema = parse_schema(r#"
namespace TestApp {
entity User;
action "read" appliesTo {
principal: [User],
resource: [User]
};
}
"#);
let loaded = LoadedPolicyStore {
metadata: create_test_metadata(),
schema: Some(schema),
schema_source_exists: true,
policies: vec![PolicyFile {
name: "test.cedar".to_string(),
content: "permit(principal, action, resource);".to_string(),
}],
templates: vec![],
entities: vec![EntityFile {
name: "users.json".to_string(),
content:
r#"[{"uid": {"type": "User", "id": "alice"}, "attrs": {}, "parents": []}]"#
.to_string(),
}],
trusted_issuers: vec![IssuerFile {
name: "issuer.json".to_string(),
content: r#"{
"id": "main",
"name": "Main Issuer",
"description": "Primary issuer",
"configuration_endpoint": "https://auth.test/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"entity_type_name": "Test::access_token"
}
}
}"#
.to_string(),
}],
};
let result = PolicyStoreManager::convert_to_legacy(loaded, true);
assert!(result.is_ok(), "Conversion failed: {:?}", result.err());
let store = result.unwrap();
assert!(store.trusted_issuers.is_some());
assert!(store.default_entities.entities().len() > 0);
let issuers = store.trusted_issuers.unwrap();
assert!(issuers.contains_key("main"));
assert_eq!(store.default_entities.entities().len(), 1);
}
}