436 lines
17 KiB
Rust
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");
|
|
}
|
|
} |