2025-10-07 10:59:52 +01:00

436 lines
17 KiB
Rust

//! Policy Testing Framework
//!
//! Comprehensive testing framework for Cedar policies with mock data and scenarios.
use control_center::policies::{PolicyEngine, PolicyRequestContext, Principal, Action, Resource, PolicyDecision};
use control_center::config::ControlCenterConfig;
use serde_json::json;
use std::collections::HashMap;
/// Test case for policy evaluation
#[derive(Debug)]
pub struct PolicyTestCase {
pub name: String,
pub description: String,
pub principal: Principal,
pub action: Action,
pub resource: Resource,
pub environment: HashMap<String, serde_json::Value>,
pub expected_decision: PolicyDecision,
pub expected_policy_id: Option<String>,
}
/// Mock data builder for testing
pub struct MockDataBuilder;
impl MockDataBuilder {
/// Create a test user principal
pub fn create_user(id: &str, roles: Vec<&str>, mfa_enabled: bool) -> Principal {
let mut attributes = HashMap::new();
attributes.insert("roles".to_string(), json!(roles));
attributes.insert("mfa_enabled".to_string(), json!(mfa_enabled));
attributes.insert("account_type".to_string(), json!("user"));
attributes.insert("authentication_status".to_string(), json!("authenticated"));
Principal {
id: id.to_string(),
entity_type: "User".to_string(),
attributes,
}
}
/// Create a service account principal
pub fn create_service_account(id: &str, service_type: &str) -> Principal {
let mut attributes = HashMap::new();
attributes.insert("account_type".to_string(), json!("service"));
attributes.insert("service_type".to_string(), json!(service_type));
attributes.insert("mfa_enabled".to_string(), json!(false));
Principal {
id: id.to_string(),
entity_type: "ServiceAccount".to_string(),
attributes,
}
}
/// Create an admin user with elevated privileges
pub fn create_admin_user(id: &str, clearance_level: &str) -> Principal {
let mut attributes = HashMap::new();
attributes.insert("roles".to_string(), json!(["Admin", "SRE"]));
attributes.insert("mfa_enabled".to_string(), json!(true));
attributes.insert("mfa_last_verified".to_string(), json!(chrono::Utc::now().timestamp() - 300)); // 5 minutes ago
attributes.insert("clearance_level".to_string(), json!(clearance_level));
attributes.insert("security_training".to_string(), json!({"completed": true, "expires_at": chrono::Utc::now().timestamp() + 86400}));
Principal {
id: id.to_string(),
entity_type: "User".to_string(),
attributes,
}
}
/// Create a test action
pub fn create_action(action_type: &str) -> Action {
let mut attributes = HashMap::new();
attributes.insert("category".to_string(), json!("system"));
Action {
id: action_type.to_string(),
entity_type: "Action".to_string(),
attributes,
}
}
/// Create a sensitive resource
pub fn create_sensitive_resource(id: &str, classification: &str, environment: &str) -> Resource {
let mut attributes = HashMap::new();
attributes.insert("classification".to_string(), json!(classification));
attributes.insert("environment".to_string(), json!(environment));
attributes.insert("criticality".to_string(), json!("high"));
Resource {
id: id.to_string(),
entity_type: "Resource".to_string(),
attributes,
}
}
/// Create production database resource
pub fn create_production_db(id: &str) -> Resource {
let mut attributes = HashMap::new();
attributes.insert("resource_type".to_string(), json!("Database"));
attributes.insert("environment".to_string(), json!("production"));
attributes.insert("classification".to_string(), json!("confidential"));
attributes.insert("data_type".to_string(), json!("financial"));
attributes.insert("requires_dual_control".to_string(), json!(true));
Resource {
id: id.to_string(),
entity_type: "Database".to_string(),
attributes,
}
}
/// Create standard business hours environment
pub fn create_business_hours_env() -> HashMap<String, serde_json::Value> {
let mut env = HashMap::new();
let now = chrono::Utc::now();
env.insert("time".to_string(), json!({
"timestamp": now.timestamp(),
"hour": 10, // 10 AM
"day_of_week": 2, // Tuesday
"utc": now.to_rfc3339()
}));
env.insert("geo".to_string(), json!({
"country": "US",
"ip": "192.168.1.100"
}));
env.insert("system".to_string(), json!({
"environment": "production",
"service": "control-center"
}));
env
}
/// Create after-hours environment
pub fn create_after_hours_env() -> HashMap<String, serde_json::Value> {
let mut env = HashMap::new();
let now = chrono::Utc::now();
env.insert("time".to_string(), json!({
"timestamp": now.timestamp(),
"hour": 22, // 10 PM
"day_of_week": 2, // Tuesday
"utc": now.to_rfc3339()
}));
env.insert("geo".to_string(), json!({
"country": "US",
"ip": "192.168.1.100"
}));
env
}
/// Create weekend environment
pub fn create_weekend_env() -> HashMap<String, serde_json::Value> {
let mut env = HashMap::new();
let now = chrono::Utc::now();
env.insert("time".to_string(), json!({
"timestamp": now.timestamp(),
"hour": 14, // 2 PM
"day_of_week": 6, // Saturday
"utc": now.to_rfc3339()
}));
env.insert("geo".to_string(), json!({
"country": "US",
"ip": "192.168.1.100"
}));
env
}
/// Create international access environment
pub fn create_international_env(country: &str) -> HashMap<String, serde_json::Value> {
let mut env = HashMap::new();
let now = chrono::Utc::now();
env.insert("time".to_string(), json!({
"timestamp": now.timestamp(),
"hour": 10,
"day_of_week": 2
}));
env.insert("geo".to_string(), json!({
"country": country,
"ip": "203.0.113.1" // Example international IP
}));
env
}
}
/// Policy test runner
pub struct PolicyTestRunner {
engine: PolicyEngine,
}
impl PolicyTestRunner {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let config = ControlCenterConfig::default();
let engine = PolicyEngine::new(config).await?;
Ok(Self { engine })
}
/// Run a single test case
pub async fn run_test(&self, test_case: PolicyTestCase) -> Result<(), Box<dyn std::error::Error>> {
let context = PolicyRequestContext {
principal: test_case.principal,
action: test_case.action,
resource: test_case.resource,
environment: test_case.environment,
};
let result = self.engine.evaluate(&context).await?;
// Assert expected decision
match (result.decision, test_case.expected_decision) {
(PolicyDecision::Allow, PolicyDecision::Allow) => {
println!("✓ Test '{}' passed: Allow decision", test_case.name);
}
(PolicyDecision::Deny, PolicyDecision::Deny) => {
println!("✓ Test '{}' passed: Deny decision", test_case.name);
}
(actual, expected) => {
panic!(
"✗ Test '{}' failed: Expected {:?}, got {:?}",
test_case.name, expected, actual
);
}
}
// Assert policy ID if specified
if let Some(expected_policy_id) = test_case.expected_policy_id {
match result.policy_id {
Some(actual_policy_id) if actual_policy_id == expected_policy_id => {
println!("✓ Test '{}' passed: Policy ID match", test_case.name);
}
Some(actual_policy_id) => {
panic!(
"✗ Test '{}' failed: Expected policy ID '{}', got '{}'",
test_case.name, expected_policy_id, actual_policy_id
);
}
None => {
panic!(
"✗ Test '{}' failed: Expected policy ID '{}', got None",
test_case.name, expected_policy_id
);
}
}
}
Ok(())
}
/// Run multiple test cases
pub async fn run_test_suite(&self, test_cases: Vec<PolicyTestCase>) -> Result<(), Box<dyn std::error::Error>> {
let mut passed = 0;
let mut failed = 0;
for test_case in test_cases {
match self.run_test(test_case).await {
Ok(()) => passed += 1,
Err(e) => {
eprintln!("Test failed: {}", e);
failed += 1;
}
}
}
println!("\nTest Results: {} passed, {} failed", passed, failed);
if failed > 0 {
return Err(format!("{} tests failed", failed).into());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mfa_policy() {
let runner = PolicyTestRunner::new().await.expect("Failed to create test runner");
let test_cases = vec![
PolicyTestCase {
name: "User with MFA accessing sensitive resource".to_string(),
description: "Should allow access when MFA is enabled".to_string(),
principal: {
let mut user = MockDataBuilder::create_user("user1", vec!["Developer"], true);
user.attributes.insert("mfa_last_verified".to_string(), json!(chrono::Utc::now().timestamp() - 300));
user
},
action: MockDataBuilder::create_action("access"),
resource: MockDataBuilder::create_sensitive_resource("sensitive-db", "sensitive", "production"),
environment: MockDataBuilder::create_business_hours_env(),
expected_decision: PolicyDecision::Allow,
expected_policy_id: None,
},
PolicyTestCase {
name: "User without MFA accessing sensitive resource".to_string(),
description: "Should deny access when MFA is not enabled".to_string(),
principal: MockDataBuilder::create_user("user2", vec!["Developer"], false),
action: MockDataBuilder::create_action("access"),
resource: MockDataBuilder::create_sensitive_resource("sensitive-db", "sensitive", "production"),
environment: MockDataBuilder::create_business_hours_env(),
expected_decision: PolicyDecision::Deny,
expected_policy_id: None,
},
PolicyTestCase {
name: "User accessing non-sensitive resource without MFA".to_string(),
description: "Should allow access to public resources without MFA".to_string(),
principal: MockDataBuilder::create_user("user3", vec!["Developer"], false),
action: MockDataBuilder::create_action("access"),
resource: MockDataBuilder::create_sensitive_resource("public-docs", "public", "production"),
environment: MockDataBuilder::create_business_hours_env(),
expected_decision: PolicyDecision::Allow,
expected_policy_id: None,
},
];
runner.run_test_suite(test_cases).await.expect("Test suite failed");
}
#[tokio::test]
async fn test_production_approval_policy() {
let runner = PolicyTestRunner::new().await.expect("Failed to create test runner");
let test_cases = vec![
PolicyTestCase {
name: "Admin with approval deploying to production".to_string(),
description: "Should allow deployment with valid approval".to_string(),
principal: {
let mut admin = MockDataBuilder::create_admin_user("admin1", "confidential");
admin.attributes.insert("approval".to_string(), json!({
"environment": "production",
"approved_by": "ProductionAdmin",
"approved_at": chrono::Utc::now().timestamp() - 3600, // 1 hour ago
"expires_at": chrono::Utc::now().timestamp() + 82800, // 23 hours from now
"change_ticket": "CHG-2024-001",
"risk_assessment": "low"
}));
admin
},
action: MockDataBuilder::create_action("deploy"),
resource: MockDataBuilder::create_production_db("prod-db-1"),
environment: MockDataBuilder::create_business_hours_env(),
expected_decision: PolicyDecision::Allow,
expected_policy_id: None,
},
PolicyTestCase {
name: "Developer without approval trying to deploy".to_string(),
description: "Should deny deployment without proper approval".to_string(),
principal: MockDataBuilder::create_user("dev1", vec!["Developer"], true),
action: MockDataBuilder::create_action("deploy"),
resource: MockDataBuilder::create_production_db("prod-db-1"),
environment: MockDataBuilder::create_business_hours_env(),
expected_decision: PolicyDecision::Deny,
expected_policy_id: None,
},
];
runner.run_test_suite(test_cases).await.expect("Test suite failed");
}
#[tokio::test]
async fn test_geographic_restrictions() {
let runner = PolicyTestRunner::new().await.expect("Failed to create test runner");
let test_cases = vec![
PolicyTestCase {
name: "US user accessing general resource".to_string(),
description: "Should allow access from approved country".to_string(),
principal: MockDataBuilder::create_user("user1", vec!["Employee"], false),
action: MockDataBuilder::create_action("read"),
resource: MockDataBuilder::create_sensitive_resource("internal-docs", "internal", "production"),
environment: MockDataBuilder::create_international_env("US"),
expected_decision: PolicyDecision::Allow,
expected_policy_id: None,
},
PolicyTestCase {
name: "Restricted country access attempt".to_string(),
description: "Should deny access from sanctioned country".to_string(),
principal: MockDataBuilder::create_user("user2", vec!["Employee"], false),
action: MockDataBuilder::create_action("read"),
resource: MockDataBuilder::create_sensitive_resource("internal-docs", "internal", "production"),
environment: MockDataBuilder::create_international_env("IR"), // Iran - sanctioned
expected_decision: PolicyDecision::Deny,
expected_policy_id: None,
},
];
runner.run_test_suite(test_cases).await.expect("Test suite failed");
}
#[tokio::test]
async fn test_time_based_access() {
let runner = PolicyTestRunner::new().await.expect("Failed to create test runner");
let test_cases = vec![
PolicyTestCase {
name: "Employee access during business hours".to_string(),
description: "Should allow access during business hours".to_string(),
principal: MockDataBuilder::create_user("emp1", vec!["Employee"], false),
action: MockDataBuilder::create_action("read"),
resource: MockDataBuilder::create_sensitive_resource("company-docs", "internal", "production"),
environment: MockDataBuilder::create_business_hours_env(),
expected_decision: PolicyDecision::Allow,
expected_policy_id: None,
},
PolicyTestCase {
name: "Employee access after hours without approval".to_string(),
description: "Should deny access outside business hours without approval".to_string(),
principal: MockDataBuilder::create_user("emp2", vec!["Employee"], false),
action: MockDataBuilder::create_action("read"),
resource: MockDataBuilder::create_sensitive_resource("company-docs", "internal", "production"),
environment: MockDataBuilder::create_after_hours_env(),
expected_decision: PolicyDecision::Deny,
expected_policy_id: None,
},
];
runner.run_test_suite(test_cases).await.expect("Test suite failed");
}
}