#[cfg(not(target_arch = "wasm32"))]
use std::fs;
#[cfg(not(target_arch = "wasm32"))]
use std::io::Read;
use std::io::Write;
use serde_json::json;
#[cfg(not(target_arch = "wasm32"))]
use tempfile::TempDir;
use tokio::test;
#[cfg(not(target_arch = "wasm32"))]
use zip::read::ZipArchive;
use crate::common::policy_store::test_utils::PolicyStoreTestBuilder;
use crate::tests::utils::cedarling_util::{get_cedarling_with_callback, get_config};
use crate::tests::utils::test_helpers::{create_test_principal, create_test_unsigned_request};
use crate::{
BootstrapConfig, Cedarling, DataStoreConfig, EntityData, PolicyStoreConfig, PolicyStoreSource,
TrustedIssuerLoadingInfo,
};
fn create_authz_policy_store_builder() -> PolicyStoreTestBuilder {
PolicyStoreTestBuilder::new("a1b2c3d4e5f6a7b8")
.with_name("Integration Test Policy Store")
.with_schema(
r#"namespace TestApp {
entity User {
name: String,
user_type: String,
};
entity Resource {
name: String,
};
action "read" appliesTo {
principal: [User],
resource: [Resource]
};
action "write" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#,
)
.with_policy(
"allow-read",
r#"@id("allow-read")
permit(
principal,
action == TestApp::Action::"read",
resource
);"#,
)
.with_policy(
"deny-write-guest",
r#"@id("deny-write-guest")
forbid(
principal,
action == TestApp::Action::"write",
resource
) when { principal.user_type == "guest" };"#,
)
}
#[cfg(not(target_arch = "wasm32"))]
fn extract_archive_to_temp_dir(archive_bytes: &[u8]) -> TempDir {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let mut zip_archive =
ZipArchive::new(std::io::Cursor::new(archive_bytes)).expect("Failed to read zip archive");
for i in 0..zip_archive.len() {
let mut file = zip_archive.by_index(i).expect("Failed to get zip entry");
let file_path = temp_dir.path().join(file.name());
if file.is_dir() {
fs::create_dir_all(&file_path).expect("Failed to create directory");
} else {
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).expect("Failed to create parent directory");
}
let mut contents = Vec::new();
file.read_to_end(&mut contents)
.expect("Failed to read file contents");
fs::write(&file_path, contents).expect("Failed to write file");
}
}
temp_dir
}
async fn get_cedarling_from_directory(path: std::path::PathBuf) -> Cedarling {
get_cedarling_with_callback(PolicyStoreSource::Directory(path), |_| {}).await
}
async fn get_cedarling_from_cjar_file(path: std::path::PathBuf) -> Cedarling {
get_cedarling_with_callback(PolicyStoreSource::CjarFile(path), |_| {}).await
}
#[cfg(not(target_arch = "wasm32"))]
const MULTI_POLICY_CJAR_TEST_SCHEMA: &str = r#"namespace TestApp {
entity User {
name: String,
user_type: String,
};
entity Resource {
name: String,
};
action "read" appliesTo {
principal: [User],
resource: [Resource]
};
action "write" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#;
#[cfg(not(target_arch = "wasm32"))]
const MULTI_POLICY_COMBINED_CEDAR: &str = r#"@id("allow-read")
permit(
principal,
action == TestApp::Action::"read",
resource
);
@id("allow-write-all")
permit(
principal,
action == TestApp::Action::"write",
resource
);
@id("deny-write-guest")
forbid(
principal,
action == TestApp::Action::"write",
resource
) when { principal.user_type == "guest" };"#;
#[cfg(not(target_arch = "wasm32"))]
async fn multi_policy_cjar_unsigned_decision(
cedarling: &Cedarling,
action: &str,
user: EntityData,
) -> bool {
let resource = create_test_principal(
"TestApp::Resource",
"resource1",
json!({"name": "Test Resource"}),
)
.expect("Failed to create resource");
let request = create_test_unsigned_request(action, Some(user), resource);
cedarling
.authorize_unsigned(request)
.await
.expect("Authorization should succeed")
.decision
}
fn create_jwt_cedarling_config(
policy_store_source: PolicyStoreSource,
jwt_sig_validation: bool,
) -> BootstrapConfig {
create_jwt_cedarling_config_with_loader(policy_store_source, jwt_sig_validation, false)
}
fn create_jwt_cedarling_config_with_loader(
policy_store_source: PolicyStoreSource,
jwt_sig_validation: bool,
async_loading: bool,
) -> BootstrapConfig {
use crate::jwt_config::{JwtConfig, TrustedIssuerLoaderConfig, WorkersCount};
use crate::{AuthorizationConfig, BootstrapConfig, LogConfig, LogTypeConfig};
let trusted_issuer_loader = if async_loading {
TrustedIssuerLoaderConfig::Async {
workers: WorkersCount::MIN,
}
} else {
TrustedIssuerLoaderConfig::Sync {
workers: WorkersCount::MIN,
}
};
BootstrapConfig {
application_name: "test_app".to_string(),
log_config: LogConfig {
log_type: LogTypeConfig::Off,
log_level: crate::LogLevel::DEBUG,
},
policy_store_config: PolicyStoreConfig {
source: policy_store_source,
..Default::default()
},
jwt_config: JwtConfig {
jwks: None,
jwt_sig_validation,
jwt_status_validation: false,
trusted_issuer_loader,
..Default::default()
}
.allow_all_algorithms(),
authorization_config: AuthorizationConfig {
decision_log_default_jwt_id: "jti".to_string(),
strict_schema_validation: true,
},
lock_config: None,
max_default_entities: None,
max_base64_size: None,
data_store_config: DataStoreConfig::default(),
http_client_config: crate::HttpClientConfig::default(),
}
}
fn create_jwt_trusted_issuer_json(oidc_endpoint: &str) -> String {
format!(
r#"{{
"id": "mock_issuer",
"name": "Jans",
"description": "Test issuer for JWT validation",
"configuration_endpoint": "{oidc_endpoint}",
"token_metadata": {{
"access_token": {{
"entity_type_name": "Jans::Access_token"
}},
"id_token": {{
"entity_type_name": "Jans::Id_token"
}},
"userinfo_token": {{
"entity_type_name": "Jans::Userinfo_token"
}}
}}
}}"#
)
}
fn create_jwt_trusted_issuer_json_with_id(issuer_id: &str, oidc_endpoint: &str) -> String {
format!(
r#"{{
"id": "{issuer_id}",
"name": "Jans",
"description": "Test issuer for JWT validation",
"configuration_endpoint": "{oidc_endpoint}",
"token_metadata": {{
"access_token": {{
"entity_type_name": "Jans::Access_token"
}},
"id_token": {{
"entity_type_name": "Jans::Id_token"
}},
"userinfo_token": {{
"entity_type_name": "Jans::Userinfo_token"
}}
}}
}}"#
)
}
const SCHEMA: &str = r#"namespace Jans {
type Url = {"host": String, "path": String, "protocol": String};
entity TrustedIssuer = {"issuer_entity_id": Url};
entity Access_token = {
aud: String,
exp: Long,
iat: Long,
iss: TrustedIssuer,
jti: String,
client_id?: String,
org_id?: String,
};
entity Id_token = {
aud: Set<String>,
exp: Long,
iat: Long,
iss: TrustedIssuer,
jti: String,
sub: String,
};
entity Userinfo_token = {
country?: String,
exp?: Long,
iat?: Long,
iss: TrustedIssuer,
jti: String,
sub: String,
role?: Set<String>,
};
entity Workload {
iss: TrustedIssuer,
access_token: Access_token,
client_id: String,
org_id?: String,
};
entity User {
userinfo_token: Userinfo_token,
country?: String,
role?: Set<String>,
sub: String,
};
entity Role;
entity Resource {
org_id?: String,
country?: String,
};
action "Read" appliesTo {
principal: [Workload, User, Role],
resource: [Resource],
context: {}
};
}
"#;
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_from_directory_and_authorize_success() {
use crate::tests::utils::test_helpers::{create_test_principal, create_test_unsigned_request};
let builder = create_authz_policy_store_builder();
let archive = builder
.build_archive()
.expect("Failed to build test archive");
let temp_dir = extract_archive_to_temp_dir(&archive);
let cedarling = get_cedarling_from_directory(temp_dir.path().to_path_buf()).await;
let request = create_test_unsigned_request(
"TestApp::Action::\"read\"",
Some(
create_test_principal(
"TestApp::User",
"user1",
json!({"name": "Test User", "user_type": "admin"}),
)
.expect("Failed to create principal"),
),
create_test_principal(
"TestApp::Resource",
"resource1",
json!({"name": "Test Resource"}),
)
.expect("Failed to create resource"),
);
let result = cedarling
.authorize_unsigned(request)
.await
.expect("Authorization should succeed");
assert!(
result.decision,
"Read action should be allowed by the allow-read policy"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_trusted_issuer_loading_info_on_cedarling() {
use crate::jwt::test_utils::MockServer;
let mock_server = MockServer::new_with_defaults()
.await
.expect("Failed to create mock server");
let issuer_url = mock_server.issuer();
let oidc_endpoint = format!("{issuer_url}/.well-known/openid-configuration");
let trusted_issuer_json = create_jwt_trusted_issuer_json(&oidc_endpoint);
let builder = PolicyStoreTestBuilder::new("a1b2c3d4e5f6a7b8")
.with_name("Loading Info Test Policy Store")
.with_schema(SCHEMA)
.with_policy(
"allow-workload-read",
r#"@id("allow-workload-read")
permit(
principal is Jans::Workload,
action == Jans::Action::"Read",
resource is Jans::Resource
)when{
principal.access_token.org_id == resource.org_id
};"#,
)
.with_trusted_issuer("mock_issuer", trusted_issuer_json);
let archive = builder.build_archive().expect("Failed to build archive");
let temp_dir = extract_archive_to_temp_dir(&archive);
let config = create_jwt_cedarling_config(
PolicyStoreSource::Directory(temp_dir.path().to_path_buf()),
true,
);
let cedarling = crate::Cedarling::new(&config)
.await
.expect("Cedarling should initialize with JWT-enabled config");
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
assert!(cedarling.is_trusted_issuer_loaded_by_name("mock_issuer"));
assert!(!cedarling.is_trusted_issuer_loaded_by_name("NonExistent"));
assert!(cedarling.is_trusted_issuer_loaded_by_iss(issuer_url.as_str()));
assert!(!cedarling.is_trusted_issuer_loaded_by_iss("https://nonexistent.com"));
assert_eq!(cedarling.loaded_trusted_issuers_count(), 1);
let loaded_ids = cedarling.loaded_trusted_issuer_ids();
assert_eq!(loaded_ids.len(), 1);
assert!(loaded_ids.contains("mock_issuer"));
let failed_ids = cedarling.failed_trusted_issuer_ids();
assert!(failed_ids.is_empty());
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_trusted_issuer_loading_info_failed_issuer() {
use crate::jwt::test_utils::MockServer;
use std::time::{Duration, Instant};
let working_mock_server = MockServer::new_with_defaults()
.await
.expect("Failed to create working mock server");
let working_issuer_url = working_mock_server.issuer();
let working_oidc_endpoint = format!("{working_issuer_url}/.well-known/openid-configuration");
let working_issuer_json =
create_jwt_trusted_issuer_json_with_id("working_issuer", &working_oidc_endpoint);
let failing_mock_server = MockServer::new_with_failing_oidc()
.await
.expect("Failed to create failing mock server");
let failing_issuer_url = failing_mock_server.issuer();
let failing_oidc_endpoint = format!("{failing_issuer_url}/.well-known/openid-configuration");
let failing_issuer_json =
create_jwt_trusted_issuer_json_with_id("failing_issuer", &failing_oidc_endpoint);
let builder = PolicyStoreTestBuilder::new("a1b2c3d4e5f6a7b8")
.with_name("Mixed Loading Info Test Policy Store")
.with_schema(SCHEMA)
.with_policy(
"allow-workload-read",
r#"@id("allow-workload-read")
permit(
principal is Jans::Workload,
action == Jans::Action::"Read",
resource is Jans::Resource
)when{
principal.access_token.org_id == resource.org_id
};"#,
)
.with_trusted_issuer("working_issuer", working_issuer_json)
.with_trusted_issuer("failing_issuer", failing_issuer_json);
let archive = builder.build_archive().expect("Failed to build archive");
let temp_dir = extract_archive_to_temp_dir(&archive);
let config = create_jwt_cedarling_config_with_loader(
PolicyStoreSource::Directory(temp_dir.path().to_path_buf()),
true,
true,
);
let cedarling = crate::Cedarling::new(&config)
.await
.expect("Cedarling should initialize with JWT-enabled config");
assert_eq!(
cedarling.total_issuers(),
2,
"Total issuers should be 2 (working and failing)"
);
let start = Instant::now();
let timeout = Duration::from_secs(5);
loop {
let loaded = cedarling.loaded_trusted_issuers_count();
let failed = cedarling.failed_trusted_issuer_ids().len();
let total = loaded + failed;
if total == 2 {
break;
}
assert!(
(start.elapsed() <= timeout),
"Timeout waiting for trusted issuers to load. Loaded: {loaded}, Failed: {failed}"
);
tokio::time::sleep(Duration::from_millis(1)).await;
}
assert!(cedarling.is_trusted_issuer_loaded_by_name("working_issuer"));
assert!(!cedarling.is_trusted_issuer_loaded_by_name("failing_issuer"));
assert!(!cedarling.is_trusted_issuer_loaded_by_name("NonExistent"));
assert!(cedarling.is_trusted_issuer_loaded_by_iss(working_issuer_url.as_str()));
assert!(!cedarling.is_trusted_issuer_loaded_by_iss(failing_issuer_url.as_str()));
assert_eq!(cedarling.loaded_trusted_issuers_count(), 1);
assert_eq!(cedarling.failed_trusted_issuer_ids().len(), 1);
let loaded_ids = cedarling.loaded_trusted_issuer_ids();
assert_eq!(loaded_ids.len(), 1);
assert!(loaded_ids.contains("working_issuer"));
assert!(!loaded_ids.contains("failing_issuer"));
let failed_ids = cedarling.failed_trusted_issuer_ids();
assert_eq!(failed_ids.len(), 1);
assert!(failed_ids.contains("failing_issuer"));
assert!(!failed_ids.contains("working_issuer"));
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_from_directory_deny_write_for_guest() {
let builder = create_authz_policy_store_builder();
let archive = builder
.build_archive()
.expect("Failed to build test archive");
let temp_dir = extract_archive_to_temp_dir(&archive);
let cedarling = get_cedarling_from_directory(temp_dir.path().to_path_buf()).await;
let request = create_test_unsigned_request(
"TestApp::Action::\"write\"",
Some(
create_test_principal(
"TestApp::User",
"guest_user",
json!({"name": "Guest User", "user_type": "guest"}),
)
.expect("Failed to create principal"),
),
create_test_principal(
"TestApp::Resource",
"resource1",
json!({"name": "Test Resource"}),
)
.expect("Failed to create resource"),
);
let result = cedarling
.authorize_unsigned(request)
.await
.expect("Authorization should succeed");
assert!(
!result.decision,
"Write action should be denied for guest users by the deny-write-guest policy"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_from_cjar_file_and_authorize_success() {
let builder = create_authz_policy_store_builder();
let archive = builder
.build_archive()
.expect("Failed to build test archive");
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let archive_path = temp_dir.path().join("test_policy_store.cjar");
fs::write(&archive_path, &archive).expect("Failed to write archive file");
let cedarling = get_cedarling_from_cjar_file(archive_path).await;
let request = create_test_unsigned_request(
"TestApp::Action::\"read\"",
Some(
create_test_principal(
"TestApp::User",
"user1",
json!({"name": "Test User", "user_type": "admin"}),
)
.expect("Failed to create principal"),
),
create_test_principal(
"TestApp::Resource",
"resource1",
json!({"name": "Test Resource"}),
)
.expect("Failed to create resource"),
);
let result = cedarling
.authorize_unsigned(request)
.await
.expect("Authorization should succeed");
assert!(
result.decision,
"Read action should be allowed by the allow-read policy"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_from_cjar_with_multi_policy_file() {
let builder = PolicyStoreTestBuilder::new("a1b2c3d4e5f6a7b8")
.with_name("Multi-policy cjar test")
.with_schema(MULTI_POLICY_CJAR_TEST_SCHEMA)
.with_policy("combined", MULTI_POLICY_COMBINED_CEDAR);
let archive = builder
.build_archive()
.expect("Failed to build test archive");
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let archive_path = temp_dir.path().join("multi_policy.cjar");
fs::write(&archive_path, &archive).expect("Failed to write archive file");
let cedarling = get_cedarling_from_cjar_file(archive_path).await;
let admin = |id, name| {
create_test_principal(
"TestApp::User",
id,
json!({"name": name, "user_type": "admin"}),
)
.expect("Failed to create principal")
};
let guest = |id, name| {
create_test_principal(
"TestApp::User",
id,
json!({"name": name, "user_type": "guest"}),
)
.expect("Failed to create principal")
};
assert!(
multi_policy_cjar_unsigned_decision(
&cedarling,
"TestApp::Action::\"read\"",
admin("user1", "Test User")
)
.await,
"Read should be allowed by the allow-read policy from the multi-policy file"
);
assert!(
multi_policy_cjar_unsigned_decision(
&cedarling,
"TestApp::Action::\"write\"",
admin("admin_user", "Admin"),
)
.await,
"Write by admin should be explicitly permitted by allow-write-all from the multi-policy file"
);
assert!(
!multi_policy_cjar_unsigned_decision(
&cedarling,
"TestApp::Action::\"write\"",
guest("guest_user", "Guest"),
)
.await,
"Write by guest should be denied by deny-write-guest from the same multi-policy file"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_from_cjar_with_multi_template_file() {
let builder = PolicyStoreTestBuilder::new("b1b2b3b4b5b6b7b8")
.with_name("Multi-template cjar test")
.with_schema(
r#"namespace TestApp {
entity User {
name: String,
};
entity Resource {
name: String,
};
action "view" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#,
)
.with_policy(
"noop",
r#"@id("noop")
permit(principal, action, resource);"#,
)
.with_template(
"tpls",
r#"@id("principal-view")
permit(
principal == ?principal,
action == TestApp::Action::"view",
resource
);
@id("resource-view")
permit(
principal,
action == TestApp::Action::"view",
resource == ?resource
);"#,
);
let archive = builder
.build_archive()
.expect("Failed to build test archive");
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let archive_path = temp_dir.path().join("multi_template.cjar");
fs::write(&archive_path, &archive).expect("Failed to write archive file");
let http_client = crate::http::HttpClient::new(crate::HttpClientConfig::default())
.expect("Should create HttpClient");
let loaded = crate::init::policy_store::load_policy_store(
&crate::PolicyStoreConfig {
source: crate::PolicyStoreSource::CjarFile(archive_path),
..Default::default()
},
&http_client,
true,
)
.await
.expect("Loading .cjar with a multi-template file should succeed");
let template_ids: Vec<String> = loaded
.store
.policies
.get_set()
.templates()
.map(|t| t.id().to_string())
.collect();
assert!(
template_ids.contains(&"principal-view".to_string())
&& template_ids.contains(&"resource-view".to_string()),
"expected both template ids in loaded PolicySet, got {template_ids:?}"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_directory_with_entities() {
let builder = PolicyStoreTestBuilder::new("e1e2e3e4e5e6e7e8")
.with_name("Entity Test Policy Store")
.with_schema(
r#"namespace TestApp {
entity User {
name: String,
department: String,
};
entity Resource {
name: String,
owner: String,
};
action "access" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#,
)
.with_policy(
"allow-same-department",
r#"@id("allow-same-department")
permit(
principal,
action == TestApp::Action::"access",
resource
);"#,
)
.with_entity(
"users",
serde_json::to_string(&json!([
{
"uid": {"type": "TestApp::User", "id": "alice"},
"attrs": {
"name": "Alice",
"department": "engineering"
},
"parents": []
}
]))
.unwrap(),
)
.with_entity(
"resources",
serde_json::to_string(&json!([
{
"uid": {"type": "TestApp::Resource", "id": "doc1"},
"attrs": {
"name": "Design Document",
"owner": "engineering"
},
"parents": []
}
]))
.unwrap(),
);
let archive = builder
.build_archive()
.expect("Failed to build test archive");
let temp_dir = extract_archive_to_temp_dir(&archive);
let cedarling = get_cedarling_from_directory(temp_dir.path().to_path_buf()).await;
let request = create_test_unsigned_request(
"TestApp::Action::\"access\"",
Some(
create_test_principal(
"TestApp::User",
"alice",
json!({"name": "Alice", "department": "engineering"}),
)
.expect("Failed to create principal"),
),
create_test_principal(
"TestApp::Resource",
"doc1",
json!({"name": "Design Document", "owner": "engineering"}),
)
.expect("Failed to create resource"),
);
let result = cedarling
.authorize_unsigned(request)
.await
.expect("Authorization should succeed");
assert!(
result.decision,
"Access should be allowed by the allow-same-department policy"
);
}
fn create_multiple_policy_store_builder() -> PolicyStoreTestBuilder {
PolicyStoreTestBuilder::new("f1f2f3f4f5f6f7f8")
.with_name("Multi-Policy Test Store")
.with_schema(
r#"namespace TestApp {
entity User {
user_role: String,
};
entity Resource;
action "read" appliesTo {
principal: [User],
resource: [Resource]
};
action "write" appliesTo {
principal: [User],
resource: [Resource]
};
action "delete" appliesTo {
principal: [User],
resource: [Resource]
};
}
"#,
)
.with_policy(
"allow-read-all",
r#"@id("allow-read-all")
permit(
principal,
action == TestApp::Action::"read",
resource
);"#,
)
.with_policy(
"allow-write-admin",
r#"@id("allow-write-admin")
permit(
principal,
action == TestApp::Action::"write",
resource
) when { principal.user_role == "admin" };"#,
)
.with_policy(
"deny-delete-all",
r#"@id("deny-delete-all")
forbid(
principal,
action == TestApp::Action::"delete",
resource
);"#,
)
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_directory_with_multiple_policies() {
let builder = create_multiple_policy_store_builder();
let archive = builder
.build_archive()
.expect("Failed to build test archive");
let temp_dir = extract_archive_to_temp_dir(&archive);
let cedarling = get_cedarling_from_directory(temp_dir.path().to_path_buf()).await;
let read_request = create_test_unsigned_request(
"TestApp::Action::\"read\"",
Some(
create_test_principal("TestApp::User", "user1", json!({"user_role": "viewer"}))
.expect("Failed to create principal"),
),
create_test_principal("TestApp::Resource", "resource1", json!({}))
.expect("Failed to create resource"),
);
let read_result = cedarling
.authorize_unsigned(read_request)
.await
.expect("Read authorization should succeed");
assert!(read_result.decision, "Read should be allowed for any user");
let write_admin_request = create_test_unsigned_request(
"TestApp::Action::\"write\"",
Some(
create_test_principal("TestApp::User", "admin1", json!({"user_role": "admin"}))
.expect("Failed to create principal"),
),
create_test_principal("TestApp::Resource", "resource1", json!({}))
.expect("Failed to create resource"),
);
let write_admin_result = cedarling
.authorize_unsigned(write_admin_request)
.await
.expect("Write authorization should succeed");
assert!(
write_admin_result.decision,
"Write should be allowed for admin"
);
let write_viewer_request = create_test_unsigned_request(
"TestApp::Action::\"write\"",
Some(
create_test_principal("TestApp::User", "user1", json!({"user_role": "viewer"}))
.expect("Failed to create principal"),
),
create_test_principal("TestApp::Resource", "resource1", json!({}))
.expect("Failed to create resource"),
);
let write_viewer_result = cedarling
.authorize_unsigned(write_viewer_request)
.await
.expect("Write authorization should succeed");
assert!(
!write_viewer_result.decision,
"Write should be denied for non-admin"
);
let delete_request = create_test_unsigned_request(
"TestApp::Action::\"delete\"",
Some(
create_test_principal("TestApp::User", "admin1", json!({"user_role": "admin"}))
.expect("Failed to create principal"),
),
create_test_principal("TestApp::Resource", "resource1", json!({}))
.expect("Failed to create resource"),
);
let delete_result = cedarling
.authorize_unsigned(delete_request)
.await
.expect("Delete authorization should succeed");
assert!(
!delete_result.decision,
"Delete should be denied for everyone"
);
}
#[test]
async fn test_load_from_cjar_url_and_authorize_success() {
use mockito::Server;
let builder = create_authz_policy_store_builder();
let archive_bytes = builder
.build_archive()
.expect("Failed to build test archive");
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/policy-store.cjar")
.with_status(200)
.with_header("content-type", "application/octet-stream")
.with_body(archive_bytes)
.create_async()
.await;
let cjar_url = format!("{}/policy-store.cjar", server.url());
let cedarling = get_cedarling_with_callback(PolicyStoreSource::CjarUrl(cjar_url), |_| {}).await;
mock.assert_async().await;
let request = create_test_unsigned_request(
"TestApp::Action::\"read\"",
Some(
create_test_principal(
"TestApp::User",
"user1",
json!({"name": "Test User", "user_type": "admin"}),
)
.expect("Failed to create principal"),
),
create_test_principal(
"TestApp::Resource",
"resource1",
json!({"name": "Test Resource"}),
)
.expect("Failed to create resource"),
);
let result = cedarling
.authorize_unsigned(request)
.await
.expect("Authorization should succeed");
assert!(
result.decision,
"Read action should be allowed when loading from CjarUrl"
);
}
#[test]
async fn test_cjar_url_handles_http_error() {
use super::utils::cedarling_util::get_config;
use mockito::Server;
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/nonexistent.cjar")
.with_status(404)
.with_body("Not Found")
.expect_at_least(1)
.create_async()
.await;
let cjar_url = format!("{}/nonexistent.cjar", server.url());
let config = get_config(PolicyStoreSource::CjarUrl(cjar_url));
let err = Cedarling::new(&config)
.await
.err()
.expect("Cedarling initialization should fail after retries on 404 error");
mock.assert_async().await;
assert!(
matches!(
&err,
crate::InitCedarlingError::ServiceConfig(
crate::init::service_config::ServiceConfigError::PolicyStore(
crate::init::policy_store::PolicyStoreLoadError::Archive(_)
)
)
),
"Expected Archive error after retries, got: {err:?}"
);
}
#[test]
async fn test_load_policy_store_archive_bytes_directly() {
use crate::common::policy_store::loader::load_policy_store_archive_bytes;
let builder = create_authz_policy_store_builder();
let archive_bytes = builder
.build_archive()
.expect("Failed to build test archive");
let loaded = load_policy_store_archive_bytes(&archive_bytes, true)
.expect("Should load policy store from bytes");
assert_eq!(
loaded.metadata.policy_store.id, "a1b2c3d4e5f6a7b8",
"Policy store ID should match"
);
assert_eq!(
loaded.metadata.policy_store.name, "Integration Test Policy Store",
"Policy store name should match"
);
assert!(
!loaded.policies.is_empty(),
"Should have loaded at least one policy"
);
assert_eq!(loaded.policies.len(), 2, "Should have loaded 2 policies");
let policy_names: Vec<&str> = loaded.policies.iter().map(|p| p.name.as_str()).collect();
assert!(
policy_names.contains(&"allow-read.cedar"),
"Should have allow-read policy"
);
assert!(
policy_names.contains(&"deny-write-guest.cedar"),
"Should have deny-write-guest policy"
);
}
#[test]
async fn test_load_policy_store_archive_bytes_invalid() {
use crate::common::policy_store::loader::load_policy_store_archive_bytes;
let invalid_bytes = vec![0x00, 0x01, 0x02, 0x03];
let err = load_policy_store_archive_bytes(&invalid_bytes, true)
.expect_err("Should fail to load invalid archive bytes");
assert!(
matches!(
err,
crate::common::policy_store::errors::PolicyStoreError::Archive(_)
),
"Expected Archive error for invalid bytes, got: {err:?}"
);
}
#[test]
async fn test_load_from_uri_detects_archive() {
use mockito::Server;
let builder = create_authz_policy_store_builder();
let archive_bytes = builder
.build_archive()
.expect("Failed to build test archive");
let mut server = Server::new_async().await;
let mock = server
.mock("GET", "/policy-store")
.with_status(200)
.with_header("content-type", "application/octet-stream")
.with_body(archive_bytes)
.create_async()
.await;
let uri = format!("{}/policy-store", server.url());
let cedarling = get_cedarling_with_callback(PolicyStoreSource::Uri(uri), |_| {}).await;
mock.assert_async().await;
let request = create_test_unsigned_request(
"TestApp::Action::\"read\"",
Some(
create_test_principal(
"TestApp::User",
"user1",
json!({"name": "Test User", "user_type": "admin"}),
)
.expect("Failed to create principal"),
),
create_test_principal(
"TestApp::Resource",
"resource1",
json!({"name": "Test Resource"}),
)
.expect("Failed to create resource"),
);
let result = cedarling
.authorize_unsigned(request)
.await
.expect("Authorization should succeed");
assert!(
result.decision,
"Read action should be allowed when loading archive via Uri"
);
}
fn build_archive_without_schema(id: &str, name: &str) -> Vec<u8> {
assert!(
id.len() >= 8 && id.len() <= 64 && id.chars().all(|c| c.is_ascii_hexdigit()),
"test archive id must be hex 8-64 chars, got: {id}"
);
let mut buf = Vec::new();
{
let cursor = std::io::Cursor::new(&mut buf);
let mut zip = zip::ZipWriter::new(cursor);
let opts = <zip::write::FileOptions<zip::write::ExtendedFileOptions>>::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("metadata.json", opts.clone()).unwrap();
write!(
zip,
r#"{{"cedar_version":"4.4.0","policy_store":{{"id":"{id}","name":"{name}","version":"1.0.0"}}}}"#
)
.unwrap();
zip.start_file("policies/allow.cedar", opts).unwrap();
zip.write_all(b"@id(\"allow-all\")\npermit(principal, action, resource);")
.unwrap();
zip.finish().unwrap();
}
buf
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_directory_without_schema_strict_false_succeeds() {
let archive = build_archive_without_schema("deadbeef12345678", "No Schema Dir");
let temp_dir = extract_archive_to_temp_dir(&archive);
let cedarling = get_cedarling_with_callback(
PolicyStoreSource::Directory(temp_dir.path().to_path_buf()),
|config| {
config.authorization_config.strict_schema_validation = false;
},
)
.await;
let request = create_test_unsigned_request(
"TestApp::Action::\"read\"",
Some(
create_test_principal("TestApp::User", "user1", json!({"name": "Test User"}))
.expect("principal should build"),
),
create_test_principal("TestApp::Resource", "res1", json!({}))
.expect("resource should build"),
);
let result = cedarling
.authorize_unsigned(request)
.await
.expect("authorization should succeed without schema");
assert!(result.decision, "allow-all should permit");
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
async fn test_load_directory_without_schema_strict_true_fails() {
let archive = build_archive_without_schema("deadbeef87654321", "No Schema Dir Strict");
let temp_dir = extract_archive_to_temp_dir(&archive);
let mut config = get_config(PolicyStoreSource::Directory(temp_dir.path().to_path_buf()));
config.authorization_config.strict_schema_validation = true;
let result = Cedarling::new(&config).await;
assert!(
result.is_err(),
"Expected init to fail: strict_schema_validation=true but policy store has no schema (directory)"
);
}
#[test]
async fn test_archive_without_schema_strict_false_succeeds() {
let archive_bytes = build_archive_without_schema("deadbeefaaaabbbb", "No Schema Archive");
let cedarling = get_cedarling_with_callback(
PolicyStoreSource::ArchiveBytes(archive_bytes),
|config| {
config.authorization_config.strict_schema_validation = false;
},
)
.await;
let request = create_test_unsigned_request(
"TestApp::Action::\"read\"",
Some(
create_test_principal("TestApp::User", "user1", json!({"name": "Test User"}))
.expect("principal should build"),
),
create_test_principal("TestApp::Resource", "res1", json!({}))
.expect("resource should build"),
);
let result = cedarling
.authorize_unsigned(request)
.await
.expect("authorization should succeed without schema");
assert!(result.decision, "allow-all should permit");
}