use std::path::Path;
use super::errors::{PolicyStoreError, ValidationError};
use super::metadata::PolicyStoreMetadata;
use super::schema_parser::{ParsedSchema, SchemaFile};
use super::validator::MetadataValidator;
use super::vfs_adapter::VfsFileSystem;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn load_policy_store_directory(
path: &Path,
strict: bool,
) -> Result<LoadedPolicyStore, PolicyStoreError> {
let path_str = path
.to_str()
.ok_or_else(|| PolicyStoreError::PathNotFound {
path: path.display().to_string(),
})?
.to_string();
tokio::task::spawn_blocking(move || {
let loader = DefaultPolicyStoreLoader::new_physical();
loader.load_directory(&path_str, strict)
})
.await
.map_err(|e| {
PolicyStoreError::Io(std::io::Error::other(format!(
"Blocking task panicked: {e}"
)))
})?
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn load_policy_store_directory(
_path: &Path,
_strict: bool,
) -> Result<LoadedPolicyStore, PolicyStoreError> {
Err(super::errors::ArchiveError::WasmUnsupported.into())
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn load_policy_store_archive(
path: &Path,
strict: bool,
) -> Result<LoadedPolicyStore, PolicyStoreError> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || {
use super::archive_handler::ArchiveVfs;
let archive_vfs = ArchiveVfs::from_file(&path)?;
let loader = DefaultPolicyStoreLoader::new(archive_vfs);
let loaded_directory = loader.load_directory(".", strict)?;
Ok(loaded_directory)
})
.await
.map_err(|e| {
PolicyStoreError::Io(std::io::Error::other(format!(
"Blocking task panicked: {e}"
)))
})?
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn load_policy_store_archive(
_path: &Path,
_strict: bool,
) -> Result<LoadedPolicyStore, PolicyStoreError> {
Err(super::errors::ArchiveError::WasmUnsupported.into())
}
pub(crate) fn load_policy_store_archive_bytes(
bytes: &[u8],
strict: bool,
) -> Result<LoadedPolicyStore, PolicyStoreError> {
use super::archive_handler::ArchiveVfs;
let archive_vfs = ArchiveVfs::from_buffer(bytes.to_owned())?;
let loader = DefaultPolicyStoreLoader::new(archive_vfs);
loader.load_directory(".", strict)
}
#[derive(Debug)]
pub(crate) struct LoadedPolicyStore {
pub metadata: PolicyStoreMetadata,
pub schema: Option<ParsedSchema>,
pub schema_source_exists: bool,
pub policies: Vec<PolicyFile>,
pub templates: Vec<PolicyFile>,
pub entities: Vec<EntityFile>,
pub trusted_issuers: Vec<IssuerFile>,
}
#[derive(Debug, Clone)]
pub(crate) struct PolicyFile {
pub name: String,
pub content: String,
}
#[derive(Debug, Clone)]
pub(crate) struct EntityFile {
pub name: String,
pub content: String,
}
#[derive(Debug, Clone)]
pub(crate) struct IssuerFile {
pub name: String,
pub content: String,
}
enum SchemaSource {
SingleFile { path: String },
Directory(String),
None {
searched_file: String,
searched_dir: String,
},
}
impl SchemaSource {
fn exists(&self) -> bool {
matches!(
self,
SchemaSource::SingleFile { .. } | SchemaSource::Directory(_)
)
}
}
pub(super) struct DefaultPolicyStoreLoader<V: VfsFileSystem> {
vfs: V,
}
impl<V: VfsFileSystem> DefaultPolicyStoreLoader<V> {
pub(super) fn new(vfs: V) -> Self {
Self { vfs }
}
}
#[cfg(not(target_arch = "wasm32"))]
impl DefaultPolicyStoreLoader<super::vfs_adapter::PhysicalVfs> {
pub(super) fn new_physical() -> Self {
Self::new(super::vfs_adapter::PhysicalVfs::new())
}
}
impl<V: VfsFileSystem> DefaultPolicyStoreLoader<V> {
fn join_path(base: &str, file: &str) -> String {
if base == "." || base.is_empty() {
file.to_string()
} else {
format!("{base}/{file}")
}
}
fn validate_directory_structure(&self, dir: &str) -> Result<(), PolicyStoreError> {
if !self.vfs.exists(dir) {
return Err(PolicyStoreError::PathNotFound {
path: dir.to_string(),
});
}
if !self.vfs.is_dir(dir) {
return Err(PolicyStoreError::NotADirectory {
path: dir.to_string(),
});
}
let metadata_path = Self::join_path(dir, "metadata.json");
if !self.vfs.exists(&metadata_path) {
return Err(ValidationError::MissingRequiredFile {
file: "metadata.json".to_string(),
}
.into());
}
let policies_dir = Self::join_path(dir, "policies");
if !self.vfs.exists(&policies_dir) {
return Err(ValidationError::MissingRequiredDirectory {
directory: "policies".to_string(),
}
.into());
}
if !self.vfs.is_dir(&policies_dir) {
return Err(PolicyStoreError::NotADirectory {
path: policies_dir.clone(),
});
}
Ok(())
}
fn load_metadata(&self, dir: &str) -> Result<PolicyStoreMetadata, PolicyStoreError> {
let metadata_path = Self::join_path(dir, "metadata.json");
let bytes = self.vfs.read_file(&metadata_path).map_err(|source| {
PolicyStoreError::FileReadError {
path: metadata_path.clone(),
source,
}
})?;
let content = String::from_utf8(bytes).map_err(|e| PolicyStoreError::FileReadError {
path: metadata_path.clone(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})?;
MetadataValidator::parse_and_validate(&content).map_err(PolicyStoreError::Validation)
}
fn resolve_schema_source(&self, dir: &str) -> SchemaSource {
let schema_path = Self::join_path(dir, "schema.cedarschema");
if self.vfs.exists(&schema_path) {
return SchemaSource::SingleFile { path: schema_path };
}
let schemas_dir = Self::join_path(dir, "schemas");
if self.vfs.exists(&schemas_dir) {
return SchemaSource::Directory(schemas_dir);
}
SchemaSource::None {
searched_file: schema_path,
searched_dir: schemas_dir,
}
}
fn load_schema(&self, source: &SchemaSource) -> Result<Option<ParsedSchema>, PolicyStoreError> {
match source {
SchemaSource::SingleFile { path, .. } => {
let content = self.read_schema_file_content(path)?.ok_or_else(|| {
PolicyStoreError::Validation(ValidationError::MissingSchemaSource {
searched_file: path.clone(),
searched_dir: String::new(),
})
})?;
Ok(Some(ParsedSchema::parse(&content, "schema.cedarschema")?))
},
SchemaSource::Directory(path) => self.load_schema_from_directory(path),
SchemaSource::None { .. } => Ok(None),
}
}
fn read_schema_file_content(&self, path: &str) -> Result<Option<String>, PolicyStoreError> {
match self.vfs.read_file(path) {
Ok(bytes) => {
String::from_utf8(bytes)
.map(Some)
.map_err(|e| PolicyStoreError::FileReadError {
path: path.to_string(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(source) => Err(PolicyStoreError::FileReadError {
path: path.to_string(),
source,
}),
}
}
fn load_schema_from_directory(
&self,
path: &str,
) -> Result<Option<ParsedSchema>, PolicyStoreError> {
if !self.vfs.exists(path) {
return Ok(None);
}
if !self.vfs.is_dir(path) {
return Err(PolicyStoreError::NotADirectory {
path: path.to_string(),
});
}
let entries =
self.vfs
.read_dir(path)
.map_err(|source| PolicyStoreError::DirectoryReadError {
path: path.to_string(),
source,
})?;
let raw_files = self.read_schema_files(entries)?;
if raw_files.is_empty() {
return Err(ValidationError::EmptySchemaDirectory {
path: path.to_string(),
}
.into());
}
let parsed = Self::combine_schema_files(&raw_files)?;
Ok(Some(parsed))
}
fn read_schema_files(
&self,
entries: Vec<super::vfs_adapter::DirEntry>,
) -> Result<Vec<SchemaFile>, PolicyStoreError> {
let mut files: Vec<SchemaFile> = Vec::new();
for entry in entries {
if entry.is_dir {
continue;
}
if !entry.name.to_lowercase().ends_with(".cedarschema") {
continue;
}
let bytes = self.vfs.read_file(&entry.path).map_err(|source| {
PolicyStoreError::FileReadError {
path: entry.path.clone(),
source,
}
})?;
let content =
String::from_utf8(bytes).map_err(|e| PolicyStoreError::FileReadError {
path: entry.path.clone(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})?;
files.push(SchemaFile {
name: entry.name,
content,
});
}
files.sort_by(|a, b| a.name.cmp(&b.name));
Ok(files)
}
fn combine_schema_files(raw_files: &[SchemaFile]) -> Result<ParsedSchema, PolicyStoreError> {
ParsedSchema::parse_multiple(raw_files)
}
fn load_policies(&self, dir: &str) -> Result<Vec<PolicyFile>, PolicyStoreError> {
let policies_dir = Self::join_path(dir, "policies");
self.load_cedar_files(&policies_dir, "policy")
}
fn load_templates(&self, dir: &str) -> Result<Vec<PolicyFile>, PolicyStoreError> {
let templates_dir = Self::join_path(dir, "templates");
if !self.vfs.exists(&templates_dir) {
return Ok(Vec::new());
}
self.load_cedar_files(&templates_dir, "template")
}
fn load_entities(&self, dir: &str) -> Result<Vec<EntityFile>, PolicyStoreError> {
let entities_dir = Self::join_path(dir, "entities");
if !self.vfs.exists(&entities_dir) {
return Ok(Vec::new());
}
self.load_json_files(&entities_dir, "entity")
}
fn load_trusted_issuers(&self, dir: &str) -> Result<Vec<IssuerFile>, PolicyStoreError> {
let issuers_dir = Self::join_path(dir, "trusted-issuers");
if !self.vfs.exists(&issuers_dir) {
return Ok(Vec::new());
}
let entries = self.vfs.read_dir(&issuers_dir).map_err(|source| {
PolicyStoreError::DirectoryReadError {
path: issuers_dir.clone(),
source,
}
})?;
let mut issuers = Vec::new();
for entry in entries {
if !entry.is_dir {
if !entry.name.to_lowercase().ends_with(".json") {
return Err(ValidationError::InvalidFileExtension {
file: entry.path.clone(),
expected: ".json".to_string(),
actual: Path::new(&entry.name)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("(none)")
.to_string(),
}
.into());
}
let bytes = self.vfs.read_file(&entry.path).map_err(|source| {
PolicyStoreError::FileReadError {
path: entry.path.clone(),
source,
}
})?;
let content =
String::from_utf8(bytes).map_err(|e| PolicyStoreError::FileReadError {
path: entry.path.clone(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})?;
issuers.push(IssuerFile {
name: entry.name,
content,
});
}
}
Ok(issuers)
}
fn load_cedar_files(
&self,
dir: &str,
_file_type: &str,
) -> Result<Vec<PolicyFile>, PolicyStoreError> {
let mut files = Vec::new();
self.load_cedar_files_recursive(dir, &mut files)?;
Ok(files)
}
fn load_cedar_files_recursive(
&self,
dir: &str,
files: &mut Vec<PolicyFile>,
) -> Result<(), PolicyStoreError> {
let entries =
self.vfs
.read_dir(dir)
.map_err(|source| PolicyStoreError::DirectoryReadError {
path: dir.to_string(),
source,
})?;
for entry in entries {
if entry.is_dir {
self.load_cedar_files_recursive(&entry.path, files)?;
} else {
if !entry.name.to_lowercase().ends_with(".cedar") {
return Err(ValidationError::InvalidFileExtension {
file: entry.path.clone(),
expected: ".cedar".to_string(),
actual: Path::new(&entry.name)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("(none)")
.to_string(),
}
.into());
}
let bytes = self.vfs.read_file(&entry.path).map_err(|source| {
PolicyStoreError::FileReadError {
path: entry.path.clone(),
source,
}
})?;
let content =
String::from_utf8(bytes).map_err(|e| PolicyStoreError::FileReadError {
path: entry.path.clone(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})?;
files.push(PolicyFile {
name: entry.name,
content,
});
}
}
Ok(())
}
fn load_json_files(
&self,
dir: &str,
_file_type: &str,
) -> Result<Vec<EntityFile>, PolicyStoreError> {
let entries =
self.vfs
.read_dir(dir)
.map_err(|source| PolicyStoreError::DirectoryReadError {
path: dir.to_string(),
source,
})?;
let mut files = Vec::new();
for entry in entries {
if !entry.is_dir {
if !entry.name.to_lowercase().ends_with(".json") {
return Err(ValidationError::InvalidFileExtension {
file: entry.path.clone(),
expected: ".json".to_string(),
actual: Path::new(&entry.name)
.extension()
.and_then(|s| s.to_str())
.unwrap_or("(none)")
.to_string(),
}
.into());
}
let bytes = self.vfs.read_file(&entry.path).map_err(|source| {
PolicyStoreError::FileReadError {
path: entry.path.clone(),
source,
}
})?;
let content =
String::from_utf8(bytes).map_err(|e| PolicyStoreError::FileReadError {
path: entry.path.clone(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})?;
files.push(EntityFile {
name: entry.name,
content,
});
}
}
Ok(files)
}
pub(super) fn load_directory(
&self,
dir: &str,
strict: bool,
) -> Result<LoadedPolicyStore, PolicyStoreError> {
self.validate_directory_structure(dir)?;
let schema_source = self.resolve_schema_source(dir);
let schema_source_exists = schema_source.exists();
let metadata = self.load_metadata(dir)?;
let schema = if strict {
match &schema_source {
SchemaSource::None {
searched_file,
searched_dir,
} => {
return Err(PolicyStoreError::Validation(
ValidationError::MissingSchemaSource {
searched_file: searched_file.clone(),
searched_dir: searched_dir.clone(),
},
));
},
_ => self.load_schema(&schema_source)?,
}
} else {
self.load_schema(&schema_source)?
};
let policies = self.load_policies(dir)?;
let templates = self.load_templates(dir)?;
let entities = self.load_entities(dir)?;
let trusted_issuers = self.load_trusted_issuers(dir)?;
Ok(LoadedPolicyStore {
metadata,
schema,
schema_source_exists,
policies,
templates,
entities,
trusted_issuers,
})
}
}
#[cfg(test)]
use super::policy_parser;
#[cfg(test)]
impl<V: VfsFileSystem> DefaultPolicyStoreLoader<V> {
fn parse_policies(
policy_files: &[PolicyFile],
) -> Result<Vec<policy_parser::ParsedPolicy>, PolicyStoreError> {
let mut parsed_policies = Vec::new();
for file in policy_files {
let parsed_list = policy_parser::PolicyParser::parse_policy(&file.content, &file.name)?;
parsed_policies.extend(parsed_list);
}
Ok(parsed_policies)
}
fn parse_templates(
template_files: &[PolicyFile],
) -> Result<Vec<policy_parser::ParsedTemplate>, PolicyStoreError> {
let mut parsed_templates = Vec::new();
for file in template_files {
let parsed_list =
policy_parser::PolicyParser::parse_template(&file.content, &file.name)?;
parsed_templates.extend(parsed_list);
}
Ok(parsed_templates)
}
fn create_policy_set(
policies: Vec<policy_parser::ParsedPolicy>,
templates: Vec<policy_parser::ParsedTemplate>,
) -> Result<cedar_policy::PolicySet, PolicyStoreError> {
policy_parser::PolicyParser::create_policy_set(policies, templates)
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for DefaultPolicyStoreLoader<super::vfs_adapter::PhysicalVfs> {
fn default() -> Self {
Self::new_physical()
}
}
#[cfg(test)]
#[path = "loader_tests.rs"]
mod tests;