use super::errors::PolicyStoreError;
use std::collections::HashMap;
use std::io::{Cursor, Write};
use zip::write::{ExtendedFileOptions, FileOptions};
use zip::{CompressionMethod, ZipWriter};
pub(crate) struct PolicyStoreTestBuilder {
pub id: String,
pub name: String,
pub version: String,
pub cedar_version: String,
pub description: Option<String>,
pub schema: String,
pub policies: HashMap<String, String>,
pub templates: HashMap<String, String>,
pub entities: HashMap<String, String>,
pub trusted_issuers: HashMap<String, String>,
pub extra_files: HashMap<String, String>,
}
impl Default for PolicyStoreTestBuilder {
fn default() -> Self {
Self::new("test123456789")
}
}
impl PolicyStoreTestBuilder {
pub(crate) fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
name: "Test Policy Store".to_string(),
version: "1.0.0".to_string(),
cedar_version: "4.4.0".to_string(),
description: None,
schema: Self::default_schema(),
policies: HashMap::new(),
templates: HashMap::new(),
entities: HashMap::new(),
trusted_issuers: HashMap::new(),
extra_files: HashMap::new(),
}
}
pub(crate) fn default_schema() -> String {
r#"namespace TestApp {
entity User;
entity Resource;
entity Role;
action "read" appliesTo {
principal: [User],
resource: [Resource]
};
action "write" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#
.to_string()
}
pub(crate) fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub(crate) fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = version.into();
self
}
pub(crate) fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub(crate) fn with_schema(mut self, schema: impl Into<String>) -> Self {
self.schema = schema.into();
self
}
pub(crate) fn with_policy(
mut self,
name: impl Into<String>,
content: impl Into<String>,
) -> Self {
self.policies.insert(name.into(), content.into());
self
}
pub(crate) fn with_template(
mut self,
name: impl Into<String>,
content: impl Into<String>,
) -> Self {
self.templates.insert(name.into(), content.into());
self
}
pub(crate) fn with_entity(
mut self,
name: impl Into<String>,
content: impl Into<String>,
) -> Self {
self.entities.insert(name.into(), content.into());
self
}
pub(crate) fn with_trusted_issuer(
mut self,
name: impl Into<String>,
content: impl Into<String>,
) -> Self {
self.trusted_issuers.insert(name.into(), content.into());
self
}
pub(crate) fn build_metadata_json(&self) -> String {
let mut metadata = serde_json::json!({
"cedar_version": self.cedar_version,
"policy_store": {
"id": self.id,
"name": self.name,
"version": self.version
}
});
if let Some(desc) = &self.description {
metadata["policy_store"]["description"] = serde_json::Value::String(desc.clone());
}
serde_json::to_string_pretty(&metadata).unwrap()
}
fn build_files(&self) -> HashMap<String, Vec<u8>> {
let mut files: HashMap<String, Vec<u8>> = HashMap::new();
files.insert(
"metadata.json".to_string(),
self.build_metadata_json().into_bytes(),
);
files.insert(
"schema.cedarschema".to_string(),
self.schema.as_bytes().to_vec(),
);
for (name, content) in &self.policies {
let path = format!("policies/{name}.cedar");
files.insert(path, content.as_bytes().to_vec());
}
for (name, content) in &self.templates {
let path = format!("templates/{name}.cedar");
files.insert(path, content.as_bytes().to_vec());
}
for (name, content) in &self.entities {
let path = format!("entities/{name}.json");
files.insert(path, content.as_bytes().to_vec());
}
for (name, content) in &self.trusted_issuers {
let path = format!("trusted-issuers/{name}.json");
files.insert(path, content.as_bytes().to_vec());
}
for (path, content) in &self.extra_files {
files.insert(path.clone(), content.as_bytes().to_vec());
}
files
}
pub(crate) fn build_archive(&self) -> Result<Vec<u8>, PolicyStoreError> {
let files = self.build_files();
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
let mut zip = ZipWriter::new(cursor);
for (path, content) in files {
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file(&path, options)
.map_err(|e| PolicyStoreError::Io(std::io::Error::other(e)))?;
zip.write_all(&content).map_err(PolicyStoreError::Io)?;
}
let cursor = zip
.finish()
.map_err(|e| PolicyStoreError::Io(std::io::Error::other(e)))?;
Ok(cursor.into_inner())
}
}
pub(crate) mod fixtures {
use super::*;
pub(crate) fn minimal_valid() -> PolicyStoreTestBuilder {
PolicyStoreTestBuilder::new("abc123def456").with_policy(
"allow-all",
r#"@id("allow-all")
permit(principal, action, resource);"#,
)
}
pub(crate) fn with_multiple_policies(count: usize) -> PolicyStoreTestBuilder {
let mut builder = PolicyStoreTestBuilder::new("multipolicy123");
for i in 0..count {
builder = builder.with_policy(
format!("policy{i}"),
format!(
r#"@id("policy{i}")
permit(
principal == TestApp::User::"user{i}",
action == TestApp::Action::"read",
resource == TestApp::Resource::"res{i}"
);"#
),
);
}
builder
}
pub(crate) fn with_multiple_entities(count: usize) -> PolicyStoreTestBuilder {
let mut builder = PolicyStoreTestBuilder::new("multientity123").with_policy(
"allow-all",
r#"@id("allow-all") permit(principal, action, resource);"#,
);
let mut users = Vec::new();
for i in 0..count {
users.push(serde_json::json!({
"uid": {"type": "TestApp::User", "id": format!("user{}", i)},
"attrs": {
"name": format!("User {}", i),
"email": format!("user{}@example.com", i)
},
"parents": []
}));
}
builder = builder.with_entity("users", serde_json::to_string_pretty(&users).unwrap());
builder
}
pub(crate) fn invalid_metadata_json() -> PolicyStoreTestBuilder {
let mut builder = minimal_valid();
builder
.extra_files
.insert("metadata.json".to_string(), "{ invalid json }".to_string());
builder
}
pub(crate) fn invalid_policy_syntax() -> PolicyStoreTestBuilder {
PolicyStoreTestBuilder::new("invalidpolicy")
.with_policy("bad-policy", "permit ( principal action resource );")
}
pub(crate) fn duplicate_entity_uids() -> PolicyStoreTestBuilder {
let users1 = serde_json::json!([{
"uid": {"type": "TestApp::User", "id": "alice"},
"attrs": {},
"parents": []
}]);
let users2 = serde_json::json!([{
"uid": {"type": "TestApp::User", "id": "alice"},
"attrs": {},
"parents": []
}]);
minimal_valid()
.with_entity("users1", users1.to_string())
.with_entity("users2", users2.to_string())
}
pub(crate) fn invalid_trusted_issuer() -> PolicyStoreTestBuilder {
let issuer = serde_json::json!({
"bad-issuer": {
"name": "Missing OIDC endpoint"
}
});
minimal_valid().with_trusted_issuer("bad-issuer", issuer.to_string())
}
}
pub(super) fn create_path_traversal_archive() -> Vec<u8> {
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
let mut zip = ZipWriter::new(cursor);
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("../../../etc/passwd", options).unwrap();
zip.write_all(b"malicious content").unwrap();
zip.finish().unwrap().into_inner()
}
pub(super) fn create_corrupted_archive() -> Vec<u8> {
let mut bytes = vec![0x50, 0x4B, 0x03, 0x04]; bytes.extend_from_slice(&[0xFF; 100]); bytes
}
pub(super) fn create_deep_nested_archive(depth: usize) -> Vec<u8> {
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
let mut zip = ZipWriter::new(cursor);
let path = (0..depth).map(|_| "dir").collect::<Vec<_>>().join("/") + "/file.txt";
let options = FileOptions::<ExtendedFileOptions>::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file(&path, options).unwrap();
zip.write_all(b"deep content").unwrap();
zip.finish().unwrap().into_inner()
}
pub(super) fn create_large_policy_store(
policy_count: usize,
entity_count: usize,
issuer_count: usize,
) -> PolicyStoreTestBuilder {
let mut builder = PolicyStoreTestBuilder::new("loadtest123456");
for i in 0..policy_count {
builder = builder.with_policy(
format!("policy{i:06}"),
format!(
r#"@id("policy{:06}")
permit(
principal == TestApp::User::"user{:06}",
action == TestApp::Action::"read",
resource == TestApp::Resource::"resource{:06}"
) when {{
principal has email && principal.email like "*@example.com"
}};"#,
i,
i % entity_count,
i % 100
),
);
}
let batch_size = 1000;
let entity_batches = entity_count.div_ceil(batch_size);
for batch in 0..entity_batches {
let start = batch * batch_size;
let end = ((batch + 1) * batch_size).min(entity_count);
let entities: Vec<_> = (start..end)
.map(|i| {
serde_json::json!({
"uid": {"type": "TestApp::User", "id": format!("user{:06}", i)},
"attrs": {
"name": format!("User {}", i),
"email": format!("user{}@example.com", i),
"department": format!("dept{}", i % 10)
},
"parents": []
})
})
.collect();
builder = builder.with_entity(
format!("users_batch{batch:04}"),
serde_json::to_string(&entities).unwrap(),
);
}
for i in 0..issuer_count {
let issuer = serde_json::json!({
format!("issuer{}", i): {
"name": format!("Issuer {}", i),
"openid_configuration_endpoint": format!("https://issuer{}.example.com/.well-known/openid-configuration", i),
"token_metadata": {
"access_token": {
"entity_type_name": "issuer",
"user_id": "sub",
"required_claims": ["sub"]
}
}
}
});
builder = builder.with_trusted_issuer(format!("issuer{i}"), issuer.to_string());
}
builder
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_creates_valid_metadata() {
let builder = PolicyStoreTestBuilder::new("test123abc456")
.with_name("My Test Store")
.with_version("2.0.0")
.with_description("A test store");
let metadata_json = builder.build_metadata_json();
let metadata: serde_json::Value = serde_json::from_str(&metadata_json).unwrap();
assert_eq!(metadata["cedar_version"], "4.4.0");
assert_eq!(metadata["policy_store"]["id"], "test123abc456");
assert_eq!(metadata["policy_store"]["name"], "My Test Store");
assert_eq!(metadata["policy_store"]["version"], "2.0.0");
assert_eq!(metadata["policy_store"]["description"], "A test store");
}
#[test]
fn test_builder_creates_archive() {
let builder = fixtures::minimal_valid();
let archive = builder.build_archive().unwrap();
assert!(!archive.is_empty());
assert_eq!(&archive[0..2], &[0x50, 0x4B]); }
#[test]
fn test_fixture_with_multiple_policies() {
let builder = fixtures::with_multiple_policies(10);
assert_eq!(builder.policies.len(), 10);
}
#[test]
fn test_fixture_with_multiple_entities() {
let builder = fixtures::with_multiple_entities(100);
assert_eq!(builder.entities.len(), 1); }
#[test]
fn test_large_policy_store_creation() {
let builder = create_large_policy_store(100, 1000, 5);
assert_eq!(builder.policies.len(), 100);
assert_eq!(builder.trusted_issuers.len(), 5);
}
#[test]
fn test_path_traversal_archive() {
let archive = create_path_traversal_archive();
assert!(!archive.is_empty());
}
#[test]
fn test_corrupted_archive() {
let archive = create_corrupted_archive();
assert!(!archive.is_empty());
}
#[test]
fn test_deep_nested_archive() {
let archive = create_deep_nested_archive(50);
assert!(!archive.is_empty());
}
}