use super::super::archive_handler::ArchiveVfs;
use super::super::entity_parser::EntityParser;
use super::super::errors::{CedarParseErrorDetail, PolicyStoreError, ValidationError};
use super::super::issuer_parser::IssuerParser;
use super::super::manager::PolicyStoreManager;
use super::super::vfs_adapter::{DirEntry, MemoryVfs, PhysicalVfs, VfsFileSystem};
use super::*;
use std::fs::{self, File};
use std::io::{Cursor, Read, Write};
use std::path::{Path, PathBuf};
use tempfile::TempDir;
use zip::CompressionMethod;
use zip::write::{ExtendedFileOptions, FileOptions};
struct ErrorInjectorVfs {
inner: MemoryVfs,
deny_read_patterns: Vec<String>,
}
impl ErrorInjectorVfs {
fn new(deny_read_patterns: Vec<String>) -> Self {
Self {
inner: MemoryVfs::new(),
deny_read_patterns,
}
}
fn create_file(&self, path: &str, content: &[u8]) -> std::io::Result<()> {
self.inner.create_file(path, content)
}
fn create_dir_all(&self, path: &str) -> std::io::Result<()> {
self.inner.create_dir_all(path)
}
}
impl VfsFileSystem for ErrorInjectorVfs {
fn open_file(&self, path: &str) -> std::io::Result<Box<dyn Read + Send>> {
if self.deny_read_patterns.iter().any(|p| path.contains(p)) {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("injected error for path: {path}"),
));
}
self.inner.open_file(path)
}
fn read_dir(&self, path: &str) -> std::io::Result<Vec<DirEntry>> {
self.inner.read_dir(path)
}
fn exists(&self, path: &str) -> bool {
self.inner.exists(path)
}
fn is_dir(&self, path: &str) -> bool {
self.inner.is_dir(path)
}
}
type PhysicalLoader = DefaultPolicyStoreLoader<PhysicalVfs>;
fn create_test_policy_store(dir: &Path) -> std::io::Result<()> {
let metadata = r#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abc123def456",
"name": "Test Policy Store",
"version": "1.0.0"
}
}"#;
fs::write(dir.join("metadata.json"), metadata)?;
let schema = r#"
namespace TestApp {
entity User;
entity Resource;
action "read" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#;
fs::write(dir.join("schema.cedarschema"), schema)?;
fs::create_dir(dir.join("policies"))?;
let policy = r#"@id("test-policy")
permit(
principal == TestApp::User::"alice",
action == TestApp::Action::"read",
resource == TestApp::Resource::"doc1"
);"#;
fs::write(dir.join("policies/test-policy.cedar"), policy)?;
Ok(())
}
fn create_test_archive(
name: &str,
id: &str,
extra_policies: &[(&str, &str)],
extra_entities: &[(&str, &str)],
) -> Vec<u8> {
let options = || {
FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated)
};
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut zip = zip::ZipWriter::new(cursor);
zip.start_file("metadata.json", options()).unwrap();
write!(
zip,
r#"{{"cedar_version":"4.4.0","policy_store":{{"id":"{id}","name":"{name}","version":"1.0.0"}}}}"#
)
.unwrap();
zip.start_file("schema.cedarschema", options()).unwrap();
zip.write_all(b"namespace TestApp { entity User; entity Resource; }")
.unwrap();
zip.start_file("policies/default.cedar", options()).unwrap();
zip.write_all(b"permit(principal, action, resource);")
.unwrap();
for (policy_name, content) in extra_policies {
zip.start_file(format!("policies/{policy_name}"), options())
.unwrap();
zip.write_all(content.as_bytes()).unwrap();
}
for (entity_name, content) in extra_entities {
zip.start_file(format!("entities/{entity_name}"), options())
.unwrap();
zip.write_all(content.as_bytes()).unwrap();
}
zip.finish().unwrap();
}
archive_bytes
}
#[test]
fn test_validate_nonexistent_directory() {
let loader = DefaultPolicyStoreLoader::new_physical();
let path = PathBuf::from("/nonexistent/path");
let path_str = path.to_str().unwrap_or("/nonexistent/path");
let result = loader.validate_directory_structure(path_str);
let err = result.expect_err("Expected error for nonexistent directory");
assert!(
matches!(&err, PolicyStoreError::PathNotFound { .. })
|| matches!(&err, PolicyStoreError::Io(_)),
"Expected PathNotFound or Io error, got: {err:?}"
);
}
#[test]
fn test_validate_directory_missing_metadata() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
fs::write(dir.join("schema.cedarschema"), "test").unwrap();
fs::create_dir(dir.join("policies")).unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let result = loader.validate_directory_structure(dir.to_str().unwrap());
let err = result.expect_err("Expected error for missing metadata.json");
assert!(
matches!(
&err,
PolicyStoreError::Validation(ValidationError::MissingRequiredFile { file })
if file.contains("metadata")
),
"Expected MissingRequiredFile error for metadata.json, got: {err:?}"
);
}
#[test]
fn test_validate_directory_missing_schema_is_optional() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
fs::write(dir.join("metadata.json"), "{}").unwrap();
fs::create_dir(dir.join("policies")).unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let result = loader.validate_directory_structure(dir.to_str().unwrap());
result.expect("Expected success: schema.cedarschema is now optional");
}
#[test]
fn test_validate_directory_missing_policies_dir() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
fs::write(dir.join("metadata.json"), "{}").unwrap();
fs::write(dir.join("schema.cedarschema"), "test").unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let result = loader.validate_directory_structure(dir.to_str().unwrap());
let err = result.expect_err("Expected error for missing policies directory");
assert!(
matches!(
&err,
PolicyStoreError::Validation(ValidationError::MissingRequiredDirectory { directory })
if directory.contains("policies")
),
"Expected MissingRequiredDirectory error for policies, got: {err:?}"
);
}
#[test]
fn test_validate_directory_success() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let result = loader.validate_directory_structure(dir.to_str().unwrap());
assert!(result.is_ok());
}
#[test]
fn test_load_directory_success() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load to succeed");
assert_eq!(loaded_directory.metadata.cedar_version, "4.4.0");
assert_eq!(
loaded_directory.metadata.policy_store.name,
"Test Policy Store"
);
assert!(
loaded_directory.schema.is_some(),
"expected loaded_directory.schema to be Some but was None"
);
assert_eq!(loaded_directory.policies.len(), 1);
assert_eq!(loaded_directory.policies[0].name, "test-policy.cedar");
}
#[test]
fn test_load_directory_with_optional_components() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).unwrap();
fs::create_dir(dir.join("templates")).unwrap();
fs::write(
dir.join("templates/template1.cedar"),
"@id(\"template1\") permit(principal, action, resource);",
)
.unwrap();
fs::create_dir(dir.join("entities")).unwrap();
fs::write(dir.join("entities/users.json"), "[]").unwrap();
fs::create_dir(dir.join("trusted-issuers")).unwrap();
fs::write(dir.join("trusted-issuers/issuer1.json"), "{}").unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load with optional components to succeed");
assert_eq!(loaded_directory.templates.len(), 1);
assert_eq!(loaded_directory.entities.len(), 1);
assert_eq!(loaded_directory.trusted_issuers.len(), 1);
}
#[test]
fn test_load_directory_invalid_policy_extension() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).unwrap();
fs::write(dir.join("policies/bad.txt"), "invalid").unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let result = loader.load_directory(dir.to_str().unwrap(), true);
let err = result.expect_err("Expected error for invalid policy file extension");
assert!(
matches!(
&err,
PolicyStoreError::Validation(ValidationError::InvalidFileExtension { .. })
),
"Expected InvalidFileExtension error, got: {err:?}"
);
}
#[test]
fn test_load_directory_invalid_json() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
fs::write(dir.join("metadata.json"), "not valid json").unwrap();
fs::write(dir.join("schema.cedarschema"), "schema").unwrap();
fs::create_dir(dir.join("policies")).unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let result = loader.load_directory(dir.to_str().unwrap(), true);
let err = result.expect_err("Expected error for invalid JSON in metadata.json");
assert!(
matches!(&err, PolicyStoreError::JsonParsing { file, .. } if file.contains("metadata"))
|| matches!(
&err,
PolicyStoreError::Validation(ValidationError::MetadataJsonParseFailed { .. })
),
"Expected JsonParsing or MetadataJsonParseFailed error, got: {err:?}"
);
}
#[test]
fn test_parse_policies_success() {
let policy_files = vec![
PolicyFile {
name: "policy1.cedar".to_string(),
content: r"permit(principal, action, resource);".to_string(),
},
PolicyFile {
name: "policy2.cedar".to_string(),
content: r"forbid(principal, action, resource);".to_string(),
},
];
let result = PhysicalLoader::parse_policies(&policy_files);
let parsed = result.expect("failed to parse policies");
assert_eq!(parsed.len(), 2);
assert_eq!(parsed[0].filename, "policy1.cedar");
assert_eq!(parsed[0].id.to_string(), "policy1");
assert_eq!(parsed[1].filename, "policy2.cedar");
assert_eq!(parsed[1].id.to_string(), "policy2");
}
#[test]
fn test_parse_policies_with_id_annotation() {
let policy_files = vec![PolicyFile {
name: "my_policy.cedar".to_string(),
content: r#"
// @id("custom-id-123")
permit(
principal == User::"alice",
action == Action::"view",
resource == File::"doc.txt"
);
"#
.to_string(),
}];
let result = PhysicalLoader::parse_policies(&policy_files);
let parsed = result.expect("failed to parse policies with id annotation");
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].id.to_string(), "custom-id-123");
}
#[test]
fn test_parse_policies_invalid_syntax() {
let policy_files = vec![PolicyFile {
name: "invalid.cedar".to_string(),
content: "this is not valid cedar syntax".to_string(),
}];
let result = PhysicalLoader::parse_policies(&policy_files);
let err = result.expect_err("Expected CedarParsing error for invalid syntax");
assert!(
matches!(
&err,
PolicyStoreError::CedarParsing { file, detail: CedarParseErrorDetail::ParseError(_) }
if file == "invalid.cedar"
),
"Expected CedarParsing error with ParseError detail, got: {err:?}"
);
}
#[test]
fn test_parse_templates_success() {
let template_files = vec![PolicyFile {
name: "template1.cedar".to_string(),
content: r"permit(principal == ?principal, action, resource);".to_string(),
}];
let result = PhysicalLoader::parse_templates(&template_files);
let parsed = result.expect("failed to parse templates");
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].filename, "template1.cedar");
assert_eq!(parsed[0].template.id().to_string(), "template1");
}
#[test]
fn test_create_policy_set_integration() {
let policy_files = vec![
PolicyFile {
name: "allow.cedar".to_string(),
content: r"permit(principal, action, resource);".to_string(),
},
PolicyFile {
name: "deny.cedar".to_string(),
content: r"forbid(principal, action, resource);".to_string(),
},
];
let template_files = vec![PolicyFile {
name: "user_template.cedar".to_string(),
content: r"permit(principal == ?principal, action, resource);".to_string(),
}];
let policies = PhysicalLoader::parse_policies(&policy_files).unwrap();
let templates = PhysicalLoader::parse_templates(&template_files).unwrap();
let result = PhysicalLoader::create_policy_set(policies, templates);
assert!(result.is_ok());
let policy_set = result.unwrap();
assert!(!policy_set.is_empty());
}
#[test]
fn test_load_and_parse_policies_end_to_end() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).expect("Failed to create test policy store");
let policies_dir = dir.join("policies");
fs::write(
policies_dir.join("view_policy.cedar"),
r#"
// @id("allow-view-docs")
permit(
principal == User::"alice",
action == Action::"view",
resource == File::"document.txt"
);
"#,
)
.unwrap();
fs::write(
policies_dir.join("edit_policy.cedar"),
r#"
permit(
principal == User::"bob",
action == Action::"edit",
resource == File::"document.txt"
);
"#,
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load to succeed");
let parsed_policies = PhysicalLoader::parse_policies(&loaded_directory.policies).unwrap();
assert_eq!(parsed_policies.len(), 3);
let ids: Vec<String> = parsed_policies.iter().map(|p| p.id.to_string()).collect();
assert!(ids.contains(&"test-policy".to_string())); assert!(ids.contains(&"allow-view-docs".to_string())); assert!(ids.contains(&"edit_policy".to_string()));
let policy_set = PhysicalLoader::create_policy_set(parsed_policies, vec![]).unwrap();
assert!(!policy_set.is_empty());
}
#[test]
fn test_load_and_parse_schema_end_to_end() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).expect("Failed to create test policy store");
let schema_content = r#"
namespace PhotoApp {
entity User = {
"username": String,
"email": String,
"roles": Set<String>
};
entity Photo = {
"title": String,
"owner": User,
"public": Bool
};
entity Album = {
"name": String,
"photos": Set<Photo>
};
action "view" appliesTo {
principal: [User],
resource: [Photo, Album],
context: {
"ip_address": String
}
};
action "edit" appliesTo {
principal: [User],
resource: [Photo, Album]
};
action "delete" appliesTo {
principal: [User],
resource: [Photo, Album]
};
}
"#;
fs::write(dir.join("schema.cedarschema"), schema_content).unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load to succeed");
assert!(
loaded_directory.schema.is_some(),
"Schema should not be empty"
);
let parsed = loaded_directory
.schema
.expect("Schema should be present in loaded directory");
let schema = parsed.get_schema();
assert!(!format!("{schema:?}").is_empty());
}
#[test]
fn test_load_and_parse_entities_end_to_end() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).expect("Failed to create test policy store");
let entities_dir = dir.join("entities");
fs::create_dir(&entities_dir).unwrap();
fs::write(
entities_dir.join("users.json"),
r#"[
{
"uid": {"type": "Jans::User", "id": "alice"},
"attrs": {
"email": "alice@example.com",
"role": "admin"
},
"parents": []
},
{
"uid": {"type": "Jans::User", "id": "bob"},
"attrs": {
"email": "bob@example.com",
"role": "user"
},
"parents": []
}
]"#,
)
.unwrap();
fs::write(
entities_dir.join("roles.json"),
r#"{
"admin": {
"uid": {"type": "Jans::Role", "id": "admin"},
"attrs": {
"name": "Administrator"
},
"parents": []
}
}"#,
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load to succeed");
assert!(
!loaded_directory.entities.is_empty(),
"Entities should be loaded"
);
let mut all_entities = Vec::new();
for entity_file in &loaded_directory.entities {
let parsed_entities =
EntityParser::parse_entities(&entity_file.content, &entity_file.name, None)
.expect("Should parse entities");
all_entities.extend(parsed_entities);
}
assert_eq!(all_entities.len(), 3, "Should have 3 entities total");
let uids: Vec<String> = all_entities.iter().map(|e| e.uid.to_string()).collect();
assert!(uids.contains(&"Jans::User::\"alice\"".to_string()));
assert!(uids.contains(&"Jans::User::\"bob\"".to_string()));
assert!(uids.contains(&"Jans::Role::\"admin\"".to_string()));
let entity_store = EntityParser::create_entities_store(all_entities);
assert!(entity_store.is_ok(), "Should create entity store");
assert_eq!(
entity_store.unwrap().iter().count(),
3,
"Store should have 3 entities"
);
}
#[test]
fn test_entity_with_complex_attributes() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).expect("Failed to create test policy store");
let entities_dir = dir.join("entities");
fs::create_dir(&entities_dir).unwrap();
fs::write(
entities_dir.join("complex.json"),
r#"[
{
"uid": {"type": "Jans::User", "id": "alice"},
"attrs": {
"email": "alice@example.com",
"roles": ["admin", "developer"],
"metadata": {
"department": "Engineering",
"level": 5
},
"active": true
},
"parents": []
}
]"#,
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load to succeed");
let mut all_entities = Vec::new();
for entity_file in &loaded_directory.entities {
let parsed_entities =
EntityParser::parse_entities(&entity_file.content, &entity_file.name, None)
.expect("Should parse entities with complex attributes");
all_entities.extend(parsed_entities);
}
assert_eq!(all_entities.len(), 1);
let alice_json = all_entities[0].entity.to_json_value().unwrap();
let attrs = alice_json.get("attrs").unwrap();
assert!(attrs.get("email").is_some());
assert!(attrs.get("roles").is_some());
assert!(attrs.get("metadata").is_some());
assert!(attrs.get("active").is_some());
}
#[test]
fn test_load_and_parse_trusted_issuers_end_to_end() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).expect("Failed to create test policy store");
let issuers_dir = dir.join("trusted-issuers");
fs::create_dir(&issuers_dir).unwrap();
fs::write(
issuers_dir.join("jans.json"),
r#"{
"id": "jans_server",
"name": "Jans Authorization Server",
"description": "Primary Jans OpenID Connect Provider",
"configuration_endpoint": "https://jans.test/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"trusted": true,
"entity_type_name": "Jans::access_token"
},
"id_token": {
"trusted": true,
"entity_type_name": "Jans::id_token"
}
}
}"#,
)
.unwrap();
fs::write(
issuers_dir.join("google.json"),
r#"{
"id": "google_oauth",
"name": "Google OAuth",
"description": "Google OAuth 2.0 Provider",
"configuration_endpoint": "https://accounts.google.com/.well-known/openid-configuration",
"token_metadata": {
"id_token": {
"trusted": false,
"entity_type_name": "Google::id_token"
}
}
}"#,
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load to succeed");
assert!(
!loaded_directory.trusted_issuers.is_empty(),
"Issuers should be loaded"
);
assert_eq!(
loaded_directory.trusted_issuers.len(),
2,
"Should have 2 issuer files"
);
let mut all_issuers = Vec::new();
for issuer_file in &loaded_directory.trusted_issuers {
let parsed_issuers = IssuerParser::parse_issuer(&issuer_file.content, &issuer_file.name)
.expect("Should parse issuers");
all_issuers.extend(parsed_issuers);
}
assert_eq!(all_issuers.len(), 2, "Should have 2 issuers total");
let ids: Vec<String> = all_issuers.iter().map(|i| i.id.clone()).collect();
assert!(ids.contains(&"jans_server".to_string()));
assert!(ids.contains(&"google_oauth".to_string()));
let issuer_map = IssuerParser::create_issuer_map(all_issuers);
assert_eq!(issuer_map.len(), 2, "Map should have 2 issuers");
}
#[test]
fn test_parse_issuer_with_token_metadata() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).expect("Failed to create test policy store");
let issuers_dir = dir.join("trusted-issuers");
fs::create_dir(&issuers_dir).unwrap();
fs::write(
issuers_dir.join("comprehensive.json"),
r#"{
"id": "full_issuer",
"name": "Full Feature Issuer",
"description": "Issuer with all token types",
"configuration_endpoint": "https://full.test/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"trusted": true,
"entity_type_name": "App::access_token",
"token_id": "jti"
},
"id_token": {
"trusted": true,
"entity_type_name": "App::id_token",
"token_id": "jti"
},
"userinfo_token": {
"trusted": true,
"entity_type_name": "App::userinfo_token"
}
}
}"#,
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load to succeed");
let mut all_issuers = Vec::new();
for issuer_file in &loaded_directory.trusted_issuers {
let parsed_issuers = IssuerParser::parse_issuer(&issuer_file.content, &issuer_file.name)
.expect("Should parse issuers");
all_issuers.extend(parsed_issuers);
}
assert_eq!(all_issuers.len(), 1);
let issuer = &all_issuers[0];
assert_eq!(issuer.id, "full_issuer");
assert_eq!(issuer.issuer.token_metadata.len(), 3);
let access_token = issuer.issuer.token_metadata.get("access_token").unwrap();
assert_eq!(access_token.entity_type_name, "App::access_token");
}
#[test]
fn test_detect_duplicate_issuer_ids() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
r#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abc123def456",
"name": "Test Policy Store",
"version": "1.0.0"
}
}"#
.as_bytes(),
)
.unwrap();
vfs.create_file(
"schema.cedarschema",
r#"
namespace TestApp {
entity User;
entity Resource;
action "read" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#
.as_bytes(),
)
.unwrap();
vfs.create_file(
"policies/test_policy.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
vfs.create_file(
"trusted-issuers/file1.json",
r#"{
"id": "issuer1",
"name": "Issuer One",
"description": "First instance",
"configuration_endpoint": "https://issuer1.com/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"entity_type_name": "App::access_token"
}
}
}"#
.as_bytes(),
)
.unwrap();
vfs.create_file(
"trusted-issuers/file2.json",
r#"{
"id": "issuer1",
"name": "Issuer One Duplicate",
"description": "Duplicate instance",
"configuration_endpoint": "https://issuer1.com/.well-known/openid-configuration",
"token_metadata": {
"id_token": {
"entity_type_name": "App::id_token"
}
}
}"#
.as_bytes(),
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let loaded_directory = loader
.load_directory("/", true)
.expect("Expected in-memory directory load to succeed");
let mut all_issuers = Vec::new();
for issuer_file in &loaded_directory.trusted_issuers {
let parsed_issuers = IssuerParser::parse_issuer(&issuer_file.content, &issuer_file.name)
.expect("Should parse issuers");
all_issuers.extend(parsed_issuers);
}
let validation = IssuerParser::validate_issuers(&all_issuers);
let errors = validation.expect_err("Should detect duplicate issuer IDs");
assert_eq!(errors.len(), 1, "Should have 1 duplicate error");
assert!(
errors[0].contains("issuer1"),
"Error should mention the duplicate issuer ID 'issuer1', got: {}",
errors[0]
);
assert!(
errors[0].contains("file1.json") || errors[0].contains("file2.json"),
"Error should mention the source file, got: {}",
errors[0]
);
}
#[test]
fn test_issuer_missing_required_field() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
r#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abc123def456",
"name": "Test Policy Store",
"version": "1.0.0"
}
}"#
.as_bytes(),
)
.unwrap();
vfs.create_file("schema.cedarschema", b"namespace TestApp { entity User; }")
.unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
vfs.create_file(
"trusted-issuers/invalid.json",
r#"{
"description": "Missing name field",
"configuration_endpoint": "https://test.com/.well-known/openid-configuration"
}"#
.as_bytes(),
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let loaded_directory = loader
.load_directory("/", true)
.expect("Expected in-memory directory load to succeed");
let result = IssuerParser::parse_issuer(
&loaded_directory.trusted_issuers[0].content,
&loaded_directory.trusted_issuers[0].name,
);
let err = result.expect_err("Should fail on missing required field");
assert!(
matches!(&err, PolicyStoreError::TrustedIssuerError { .. }),
"Expected TrustedIssuerError, got: {err:?}"
);
}
#[test]
fn test_complete_policy_store_with_issuers() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_policy_store(dir).expect("Failed to create test policy store");
let entities_dir = dir.join("entities");
fs::create_dir(&entities_dir).unwrap();
fs::write(
entities_dir.join("users.json"),
r#"[
{
"uid": {"type": "Jans::User", "id": "alice"},
"attrs": {"email": "alice@example.com"},
"parents": []
}
]"#,
)
.unwrap();
let issuers_dir = dir.join("trusted-issuers");
fs::create_dir(&issuers_dir).unwrap();
fs::write(
issuers_dir.join("issuer.json"),
r#"{
"id": "main_issuer",
"name": "Main Issuer",
"description": "Primary authentication provider",
"configuration_endpoint": "https://auth.test/.well-known/openid-configuration",
"token_metadata": {
"access_token": {
"entity_type_name": "Jans::access_token"
}
}
}"#,
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new_physical();
let loaded_directory = loader
.load_directory(dir.to_str().unwrap(), true)
.expect("Expected directory load to succeed");
assert_eq!(loaded_directory.metadata.name(), "Test Policy Store");
assert!(
loaded_directory.schema.is_some(),
"expected loaded_directory.schema to be Some but was None"
);
assert!(!loaded_directory.policies.is_empty());
assert!(!loaded_directory.entities.is_empty());
assert!(!loaded_directory.trusted_issuers.is_empty());
let parsed_schema = loaded_directory
.schema
.expect("Schema should be loaded for test policy store");
let parsed_policies =
PhysicalLoader::parse_policies(&loaded_directory.policies).expect("Should parse policies");
let policy_set = PhysicalLoader::create_policy_set(parsed_policies, vec![])
.expect("Should create policy set");
let mut all_entities = Vec::new();
for entity_file in &loaded_directory.entities {
let parsed_entities = EntityParser::parse_entities(
&entity_file.content,
&entity_file.name,
None, )
.expect("Should parse entities");
all_entities.extend(parsed_entities);
}
let entity_store =
EntityParser::create_entities_store(all_entities).expect("Should create entity store");
let mut all_issuers = Vec::new();
for issuer_file in &loaded_directory.trusted_issuers {
let parsed_issuers = IssuerParser::parse_issuer(&issuer_file.content, &issuer_file.name)
.expect("Should parse issuers");
all_issuers.extend(parsed_issuers);
}
let issuer_map = IssuerParser::create_issuer_map(all_issuers);
assert!(!policy_set.is_empty());
assert_eq!(entity_store.iter().count(), 1);
assert!(!format!("{:?}", parsed_schema.get_schema()).is_empty());
assert_eq!(issuer_map.len(), 1);
assert!(issuer_map.contains_key("main_issuer"));
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_archive_vfs_end_to_end_from_file() {
let temp_dir = TempDir::new().unwrap();
let archive_path = temp_dir.path().join("complete_store.cjar");
let file = File::create(&archive_path).unwrap();
let mut zip = zip::ZipWriter::new(file);
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("metadata.json", options).unwrap();
zip.write_all(
br#"{
"cedar_version": "1.0.0",
"policy_store": {
"id": "abcdef123456",
"name": "Archive Test Store",
"version": "1.0.0"
}
}"#,
)
.unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("schema.cedarschema", options).unwrap();
zip.write_all(b"namespace TestApp { entity User; }")
.unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("policies/allow.cedar", options).unwrap();
zip.write_all(b"permit(principal, action, resource);")
.unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("entities/users.json", options).unwrap();
zip.write_all(
br#"[{
"uid": {"type": "TestApp::User", "id": "alice"},
"attrs": {},
"parents": []
}]"#,
)
.unwrap();
zip.finish().unwrap();
let archive_vfs =
ArchiveVfs::from_file(&archive_path).expect("Should create ArchiveVfs from .cjar file");
let loader = DefaultPolicyStoreLoader::new(archive_vfs);
let loaded_directory = loader
.load_directory(".", true)
.expect("Should load policy store from archive");
assert_eq!(loaded_directory.metadata.name(), "Archive Test Store");
assert_eq!(loaded_directory.metadata.policy_store.id, "abcdef123456");
assert!(
loaded_directory.schema.is_some(),
"expected loaded_directory.schema to be Some but was None"
);
assert_eq!(loaded_directory.policies.len(), 1);
assert_eq!(loaded_directory.policies[0].name, "allow.cedar");
assert_eq!(loaded_directory.entities.len(), 1);
assert_eq!(loaded_directory.entities[0].name, "users.json");
let parsed_schema = loaded_directory.schema.expect("Should have parsed schema");
let parsed_entities = EntityParser::parse_entities(
&loaded_directory.entities[0].content,
"users.json",
Some(parsed_schema.get_schema()),
)
.expect("Should parse entities from archive");
assert_eq!(parsed_entities.len(), 1);
}
#[test]
fn test_archive_vfs_end_to_end_from_bytes() {
let archive_bytes = create_test_archive("WASM Archive Store", "fedcba654321", &[], &[]);
let archive_vfs =
ArchiveVfs::from_buffer(archive_bytes).expect("Should create ArchiveVfs from bytes");
let loader = DefaultPolicyStoreLoader::new(archive_vfs);
let loaded_directory = loader
.load_directory(".", true)
.expect("Should load policy store from archive bytes");
assert_eq!(loaded_directory.metadata.name(), "WASM Archive Store");
assert_eq!(loaded_directory.metadata.policy_store.id, "fedcba654321");
assert!(
loaded_directory.schema.is_some(),
"expected loaded_directory.schema to be Some but was None"
);
assert_eq!(loaded_directory.policies.len(), 1);
}
#[test]
fn test_archive_vfs_with_multiple_policies() {
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut zip = zip::ZipWriter::new(cursor);
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("metadata.json", options).unwrap();
zip.write_all(
br#"{
"cedar_version": "1.0.0",
"policy_store": {
"id": "def456abc123",
"name": "Nested Structure",
"version": "1.0.0"
}
}"#,
)
.unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("schema.cedarschema", options).unwrap();
zip.write_all(b"namespace App { entity User; }").unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("policies/allow/basic.cedar", options)
.unwrap();
zip.write_all(b"permit(principal, action, resource);")
.unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("policies/allow/advanced.cedar", options)
.unwrap();
zip.write_all(b"permit(principal == App::User::\"admin\", action, resource);")
.unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("policies/deny/restricted.cedar", options)
.unwrap();
zip.write_all(b"forbid(principal, action, resource);")
.unwrap();
zip.finish().unwrap();
}
let archive_vfs = ArchiveVfs::from_buffer(archive_bytes).expect("Should create ArchiveVfs");
let loader = DefaultPolicyStoreLoader::new(archive_vfs);
let loaded_directory = loader.load_directory(".", true).expect("Should load policies");
assert_eq!(loaded_directory.policies.len(), 3);
let policy_names: Vec<_> = loaded_directory.policies.iter().map(|p| &p.name).collect();
assert!(policy_names.contains(&&"basic.cedar".to_string()));
assert!(policy_names.contains(&&"advanced.cedar".to_string()));
assert!(policy_names.contains(&&"restricted.cedar".to_string()));
}
#[test]
fn test_archive_vfs_vs_physical_vfs_equivalence() {
let metadata_json = br#"{
"cedar_version": "1.0.0",
"policy_store": {
"id": "fedcba987654",
"name": "Equivalence Test",
"version": "1.0.0"
}
}"#;
let schema_content = b"namespace Equiv { entity User; }";
let policy_content = b"permit(principal, action, resource);";
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut zip = zip::ZipWriter::new(cursor);
let options = || -> FileOptions<ExtendedFileOptions> {
FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated)
};
zip.start_file("metadata.json", options())
.expect("Should create metadata.json in archive");
zip.write_all(metadata_json).expect("Should write metadata");
zip.start_file("schema.cedarschema", options())
.expect("Should create schema.cedarschema in archive");
zip.write_all(schema_content).expect("Should write schema content");
zip.start_file("policies/test.cedar", options())
.expect("Should create test.cedar in archive");
zip.write_all(policy_content).expect("Should write policy content");
zip.finish().expect("Should finalize archive");
}
let archive_vfs =
ArchiveVfs::from_buffer(archive_bytes).expect("Should create ArchiveVfs from bytes");
let loader = DefaultPolicyStoreLoader::new(archive_vfs);
let loaded_directory = loader
.load_directory(".", true)
.expect("Should load directory from archive");
assert_eq!(loaded_directory.metadata.policy_store.id, "fedcba987654");
assert_eq!(loaded_directory.metadata.name(), "Equivalence Test");
assert_eq!(loaded_directory.policies.len(), 1);
let parsed_schema = loaded_directory
.schema
.as_ref()
.expect("Schema should be loaded from archive");
let type_names: Vec<_> = parsed_schema
.get_schema()
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
type_names.contains(&"Equiv::User".to_string()),
"Archive schema should contain Equiv::User; got: {type_names:?}"
);
}
#[test]
fn test_load_schema_from_schemas_directory() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "Schema Dir Test",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file(
"schemas/users.cedarschema",
b"namespace App { entity User; }",
)
.unwrap();
vfs.create_file(
"schemas/resources.cedarschema",
b"namespace App { entity Resource; }",
)
.unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true).expect("Should succeed");
let parsed_schema = result
.schema
.expect("Schema should be present from schemas/ directory with User and Resource");
let entity_types: Vec<_> = parsed_schema
.get_schema()
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
entity_types.contains(&"App::User".to_string()),
"Should contain App::User; got: {entity_types:?}"
);
assert!(
entity_types.contains(&"App::Resource".to_string()),
"Should contain App::Resource; got: {entity_types:?}"
);
}
#[test]
fn test_load_schema_from_schemas_directory_deterministic_order() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "12345678abcd",
"name": "Order Test",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file(
"schemas/z_last.cedarschema",
b"namespace Z { entity ZEntity; }",
)
.unwrap();
vfs.create_file(
"schemas/a_first.cedarschema",
b"namespace A { entity AEntity; }",
)
.unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true).expect("Should succeed");
let parsed_schema = result
.schema
.expect("Schema should be present for deterministic order test");
let fragment = parsed_schema.get_fragment();
let json_str = fragment.clone().to_json_string().expect("json");
let a_pos = json_str.find("AEntity").expect("Should find AEntity");
let z_pos = json_str.find("ZEntity").expect("Should find ZEntity");
assert!(
a_pos < z_pos,
"a_first.cedarschema should come before z_last.cedarschema (sorted by filename)"
);
}
#[test]
fn test_load_schema_single_file_takes_precedence_over_schemas_dir() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "Precedence Test",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file(
"schema.cedarschema",
b"namespace Single { entity FromSingleFile; }",
)
.unwrap();
vfs.create_file(
"schemas/should_be_ignored.cedarschema",
b"namespace Ignored { entity ShouldNotAppear; }",
)
.unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true).expect("Should succeed");
let parsed_schema = result
.schema
.expect("Schema should be present from single file (precedence test)");
let entity_types: Vec<_> = parsed_schema
.get_schema()
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
entity_types.contains(&"Single::FromSingleFile".to_string()),
"Should use single file content; got: {entity_types:?}"
);
assert!(
!entity_types.contains(&"Ignored::ShouldNotAppear".to_string()),
"Should NOT include schemas/ directory content when single file exists"
);
}
#[test]
fn test_load_schema_from_schemas_dir_empty_directory_returns_none() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "Empty Dir Test",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_dir_all("schemas").unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true);
let err = result.expect_err("Expected EmptySchemaDirectory error for empty schemas/ dir");
assert!(
matches!(
&err,
PolicyStoreError::Validation(ValidationError::EmptySchemaDirectory { .. })
),
"Expected EmptySchemaDirectory error, got: {err:?}"
);
}
#[test]
fn test_load_schema_from_schemas_dir_invalid_extension() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "Extension Test",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file(
"schemas/valid.cedarschema",
b"namespace App { entity User; }",
)
.unwrap();
vfs.create_file("schemas/invalid.txt", b"namespace App { entity Resource; }")
.unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader
.load_directory(".", true)
.expect("Should skip non-.cedarschema files");
let parsed_schema = result.schema.expect("Schema should be present");
let entity_types: Vec<_> = parsed_schema
.get_schema()
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
entity_types.contains(&"App::User".to_string()),
"Should contain App::User; got: {entity_types:?}"
);
}
#[test]
fn test_load_schema_from_schemas_dir_with_multiple_namespaces() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "Multi Namespace",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file(
"schemas/users.cedarschema",
b"namespace Users { entity User; }",
)
.unwrap();
vfs.create_file(
"schemas/docs.cedarschema",
b"namespace Docs { entity Document; }",
)
.unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true).expect("Should succeed");
let parsed_schema = result
.schema
.expect("Schema should be present for multi-namespace test");
let entity_types: Vec<_> = parsed_schema
.get_schema()
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
entity_types.contains(&"Users::User".to_string()),
"Should contain Users::User; got: {entity_types:?}"
);
assert!(
entity_types.contains(&"Docs::Document".to_string()),
"Should contain Docs::Document; got: {entity_types:?}"
);
}
#[test]
fn test_load_schema_from_schemas_dir_in_archive() {
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut zip = zip::ZipWriter::new(cursor);
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("metadata.json", options).unwrap();
zip.write_all(
br#"{
"cedar_version": "1.0.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "Archive Schema Dir",
"version": "1.0.0"
}
}"#,
)
.unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("schemas/part1.cedarschema", options)
.unwrap();
zip.write_all(b"namespace Part1 { entity A; }").unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("schemas/part2.cedarschema", options)
.unwrap();
zip.write_all(b"namespace Part2 { entity B; }").unwrap();
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("policies/test.cedar", options).unwrap();
zip.write_all(b"permit(principal, action, resource);")
.unwrap();
zip.finish().unwrap();
}
let archive_vfs =
ArchiveVfs::from_buffer(archive_bytes).expect("Should create ArchiveVfs from bytes");
let loader = DefaultPolicyStoreLoader::new(archive_vfs);
let result = loader
.load_directory(".", true)
.expect("Archive load should succeed");
let parsed_schema = result
.schema
.expect("Schema should be present from schemas/ dir in archive");
let entity_types: Vec<_> = parsed_schema
.get_schema()
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
entity_types.contains(&"Part1::A".to_string()),
"Should contain Part1::A; got: {entity_types:?}"
);
assert!(
entity_types.contains(&"Part2::B".to_string()),
"Should contain Part2::B; got: {entity_types:?}"
);
}
#[test]
fn test_load_schema_from_schemas_dir_not_a_directory() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "NotADirTest",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file("schemas", b"not a directory").unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true);
let err = result.expect_err("Expected NotADirectory error");
assert!(
matches!(&err, PolicyStoreError::NotADirectory { path } if path.contains("schemas")),
"Expected NotADirectory for 'schemas' path, got: {err:?}"
);
}
#[test]
fn test_load_schema_from_schemas_dir_io_error() {
let vfs = ErrorInjectorVfs::new(vec!["schemas".to_string()]);
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "IOErrTest",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file(
"schemas/test.cedarschema",
b"namespace App { entity User; }",
)
.unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true);
let err = result.expect_err("Expected error when IO fails on schemas/ dir");
assert!(
matches!(&err, PolicyStoreError::FileReadError { path, .. } if path.contains("schemas")),
"Expected FileReadError for schemas/ path, got: {err:?}"
);
}
#[test]
fn test_load_schema_from_schemas_dir_empty_file() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "EmptyFileTest",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file("schemas/empty.cedarschema", b"").unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader
.load_directory(".", true)
.expect("Empty file is valid Cedar schema fragment");
let parsed_schema = result
.schema
.expect("Schema should be present for empty file test");
assert_eq!(
parsed_schema.get_schema().entity_types().count(),
0,
"Empty schema should have no entity types"
);
}
#[test]
fn test_load_schema_from_schemas_dir_mixed_extensions() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "MixedExtTest",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file(
"schemas/valid.cedarschema",
b"namespace App { entity User; }",
)
.unwrap();
vfs.create_file("schemas/bad.txt", b"namespace App { entity Admin; }")
.unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true).expect("Should skip .txt files");
let parsed_schema = result.schema.expect("Schema should be present");
let entity_types: Vec<_> = parsed_schema
.get_schema()
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
entity_types.contains(&"App::User".to_string()),
"Should contain App::User; got: {entity_types:?}"
);
assert!(
!entity_types.contains(&"App::Admin".to_string()),
"Should NOT contain App::Admin from bad.txt; got: {entity_types:?}"
);
}
#[test]
fn test_load_directory_schema_read_error_propagated() {
let vfs = ErrorInjectorVfs::new(vec!["schema".to_string()]);
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef123456",
"name": "Error Test",
"version": "1.0.0"
}
}"#,
)
.unwrap();
vfs.create_file("schema.cedarschema", b"namespace Test { entity User; }")
.unwrap();
vfs.create_dir_all("policies").unwrap();
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.unwrap();
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader.load_directory(".", true);
let err = result
.expect_err("Expected FileReadError when schema file read returns non-NotFound IO error");
assert!(
matches!(&err, PolicyStoreError::FileReadError { path, .. } if path.contains("schema")),
"Expected FileReadError for schema file, got: {err:?}"
);
}
#[test]
fn test_load_schema_from_schemas_dir_shared_namespace_full_pipeline() {
let vfs = MemoryVfs::new();
vfs.create_file(
"metadata.json",
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcdef1234567890",
"name": "Shared Namespace Pipeline",
"version": "1.0.0"
}
}"#,
)
.expect("Should create metadata.json");
vfs.create_file(
"schemas/users.cedarschema",
b"namespace App { entity User; }",
)
.expect("Should create users.cedarschema");
vfs.create_file(
"schemas/resources.cedarschema",
b"namespace App { entity Resource; entity Admin; }",
)
.expect("Should create resources.cedarschema");
vfs.create_file(
"policies/test.cedar",
b"permit(principal, action, resource);",
)
.expect("Should create test.cedar");
let loader = DefaultPolicyStoreLoader::new(vfs);
let result = loader
.load_directory(".", true)
.expect("Should load directory with shared namespace schemas");
let policy_store = PolicyStoreManager::convert_to_legacy(result, false)
.expect("convert_to_legacy should succeed with shared namespaces across files");
let cedar_schema = policy_store
.schema
.as_ref()
.expect("Schema should be present after conversion");
let type_names: Vec<_> = cedar_schema
.schema
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
type_names.contains(&"App::User".to_string()),
"Multi-file schema should produce App::User; got: {type_names:?}"
);
assert!(
type_names.contains(&"App::Resource".to_string()),
"Multi-file schema should produce App::Resource; got: {type_names:?}"
);
assert!(
type_names.contains(&"App::Admin".to_string()),
"Multi-file schema should produce App::Admin; got: {type_names:?}"
);
}
#[test]
fn test_archive_shared_namespace_full_pipeline() {
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut zip = zip::ZipWriter::new(cursor);
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("metadata.json", options.clone())
.expect("Should create metadata.json in archive");
zip.write_all(
br#"{
"cedar_version": "4.4.0",
"policy_store": {
"id": "abcd1234ef567890",
"name": "Archive Shared Namespace",
"version": "1.0.0"
}
}"#,
)
.expect("Should write metadata content");
zip.start_file("schemas/users.cedarschema", options.clone())
.expect("Should create users.cedarschema in archive");
zip.write_all(b"namespace App { entity User; }")
.expect("Should write User schema content");
zip.start_file("schemas/resources.cedarschema", options.clone())
.expect("Should create resources.cedarschema in archive");
zip.write_all(b"namespace App { entity Resource; entity Admin; }")
.expect("Should write Resource/Admin schema content");
zip.start_file("policies/test.cedar", options.clone())
.expect("Should create test.cedar in archive");
zip.write_all(b"permit(principal, action, resource);")
.expect("Should write policy content");
zip.finish()
.expect("Should finalize archive");
}
let archive_vfs =
ArchiveVfs::from_buffer(archive_bytes).expect("Should create ArchiveVfs from bytes");
let loader = DefaultPolicyStoreLoader::new(archive_vfs);
let result = loader
.load_directory(".", true)
.expect("Should load archive with shared namespace schemas");
let policy_store = PolicyStoreManager::convert_to_legacy(result, false)
.expect("convert_to_legacy should succeed for archive with shared namespaces");
let cedar_schema = policy_store
.schema
.as_ref()
.expect("Schema should be present after conversion from archive");
let type_names: Vec<_> = cedar_schema
.schema
.entity_types()
.map(std::string::ToString::to_string)
.collect();
assert!(
type_names.contains(&"App::User".to_string()),
"Archive schema should contain App::User; got: {type_names:?}"
);
assert!(
type_names.contains(&"App::Resource".to_string()),
"Archive schema should contain App::Resource; got: {type_names:?}"
);
assert!(
type_names.contains(&"App::Admin".to_string()),
"Archive schema should contain App::Admin; got: {type_names:?}"
);
}