use crate::{
enforcement::PolicyEnforcer,
policy::{AccessPolicy, ColumnFilter, RowFilter, RowFilterOperator, StreamFilter},
roles::Role,
};
use kimberlite_types::TenantId;
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(5)]
fn verify_role_separation() {
let tenant_a = TenantId::new(1);
let tenant_b = TenantId::new(2);
let policy_a = AccessPolicy::new(Role::User)
.with_tenant(tenant_a)
.allow_stream("data")
.allow_column("*")
.with_row_filter(RowFilter::new(
"tenant_id",
RowFilterOperator::Eq,
u64::from(tenant_a).to_string(),
));
let policy_b = AccessPolicy::new(Role::User)
.with_tenant(tenant_b)
.allow_stream("data")
.allow_column("*")
.with_row_filter(RowFilter::new(
"tenant_id",
RowFilterOperator::Eq,
u64::from(tenant_b).to_string(),
));
assert_eq!(policy_a.tenant_id, Some(tenant_a));
assert_ne!(policy_a.tenant_id, Some(tenant_b));
assert_eq!(policy_b.tenant_id, Some(tenant_b));
assert_ne!(policy_b.tenant_id, Some(tenant_a));
assert_eq!(policy_a.row_filters().len(), 1);
assert_eq!(policy_b.row_filters().len(), 1);
let filter_a = &policy_a.row_filters()[0];
let filter_b = &policy_b.row_filters()[0];
assert_eq!(filter_a.column, "tenant_id");
assert_eq!(filter_b.column, "tenant_id");
assert_eq!(filter_a.operator, RowFilterOperator::Eq);
assert_eq!(filter_b.operator, RowFilterOperator::Eq);
assert_ne!(filter_a.value, filter_b.value);
}
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(8)]
fn verify_column_filter_completeness() {
let policy = AccessPolicy::new(Role::Analyst)
.allow_stream("users")
.allow_column("*")
.deny_column("ssn")
.deny_column("password");
let enforcer = PolicyEnforcer::new(policy).without_audit();
let requested = vec![
"name".to_string(),
"email".to_string(),
"ssn".to_string(), "password".to_string(), "age".to_string(),
];
let allowed = enforcer.filter_columns(&requested);
assert!(!allowed.contains(&"ssn".to_string()));
assert!(!allowed.contains(&"password".to_string()));
assert!(allowed.contains(&"name".to_string()));
assert!(allowed.contains(&"email".to_string()));
assert!(allowed.contains(&"age".to_string()));
assert_eq!(allowed.len(), 3);
}
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(5)]
fn verify_row_filter_enforcement() {
let tenant_id = TenantId::new(42);
let policy = AccessPolicy::new(Role::User)
.with_tenant(tenant_id)
.allow_stream("*")
.allow_column("*")
.with_row_filter(RowFilter::new(
"tenant_id",
RowFilterOperator::Eq,
u64::from(tenant_id).to_string(),
));
let enforcer = PolicyEnforcer::new(policy).without_audit();
let filters = enforcer.row_filters();
assert!(!filters.is_empty());
assert_eq!(filters[0].column, "tenant_id");
assert_eq!(filters[0].operator, RowFilterOperator::Eq);
assert_eq!(filters[0].value, "42");
let where_clause = enforcer.generate_where_clause().unwrap();
assert!(!where_clause.is_empty());
assert!(where_clause.contains("tenant_id"));
assert!(where_clause.contains("42"));
let policy_multi = AccessPolicy::new(Role::User)
.allow_stream("*")
.allow_column("*")
.with_row_filter(RowFilter::new("tenant_id", RowFilterOperator::Eq, "42"))
.with_row_filter(RowFilter::new("status", RowFilterOperator::Eq, "'active'"));
let enforcer_multi = PolicyEnforcer::new(policy_multi).without_audit();
let where_multi = enforcer_multi.generate_where_clause().unwrap();
assert!(where_multi.contains("AND"));
}
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(5)]
fn verify_audit_completeness() {
let policy = AccessPolicy::new(Role::Admin)
.allow_stream("*")
.allow_column("*");
let enforcer_with_audit = PolicyEnforcer::new(policy.clone());
let enforcer_without_audit = PolicyEnforcer::new(policy).without_audit();
assert_eq!(enforcer_with_audit.policy().role, Role::Admin);
assert_eq!(enforcer_without_audit.policy().role, Role::Admin);
let result_with = enforcer_with_audit.enforce_stream_access("test");
let result_without = enforcer_without_audit.enforce_stream_access("test");
assert!(result_with.is_ok());
assert!(result_without.is_ok());
}
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(5)]
fn verify_stream_filter_logic() {
let stream_filter_allow = StreamFilter::new("test_stream", true);
let stream_filter_deny = StreamFilter::new("test_stream", false);
assert_eq!(stream_filter_allow.allow, true);
assert!(stream_filter_allow.matches("test_stream"));
assert_eq!(stream_filter_deny.allow, false);
assert!(stream_filter_deny.matches("test_stream"));
let wildcard_filter = StreamFilter::new("test_*", true);
assert!(wildcard_filter.matches("test_stream"));
assert!(wildcard_filter.matches("test_data"));
assert!(!wildcard_filter.matches("other_stream"));
}
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(5)]
fn verify_column_filter_logic() {
let column_filter_allow = ColumnFilter::new("email", true);
let column_filter_deny = ColumnFilter::new("ssn", false);
assert_eq!(column_filter_allow.allow, true);
assert!(column_filter_allow.matches("email"));
assert_eq!(column_filter_deny.allow, false);
assert!(column_filter_deny.matches("ssn"));
let prefix_filter = ColumnFilter::new("pii_*", false);
assert!(prefix_filter.matches("pii_ssn"));
assert!(prefix_filter.matches("pii_address"));
assert!(!prefix_filter.matches("public_name"));
}
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(5)]
fn verify_policy_lookup_isolated_per_tenant() {
let a_raw: u64 = kani::any();
let b_raw: u64 = kani::any();
kani::assume(a_raw != b_raw);
let tenant_a = TenantId::new(a_raw);
let tenant_b = TenantId::new(b_raw);
let policy_a = AccessPolicy::new(Role::User)
.with_tenant(tenant_a)
.allow_stream("shared_data")
.allow_column("name")
.with_row_filter(RowFilter::new(
"tenant_id",
RowFilterOperator::Eq,
u64::from(tenant_a).to_string(),
));
let policy_b = AccessPolicy::new(Role::User)
.with_tenant(tenant_b)
.allow_stream("shared_data")
.allow_column("name")
.with_row_filter(RowFilter::new(
"tenant_id",
RowFilterOperator::Eq,
u64::from(tenant_b).to_string(),
));
let enforcer_a = PolicyEnforcer::new(policy_a.clone()).without_audit();
let enforcer_b = PolicyEnforcer::new(policy_b.clone()).without_audit();
let (allowed_a, where_a) = enforcer_a
.enforce_query("shared_data", &["name".to_string()])
.expect("tenant A can access its shared_data");
let (allowed_b, where_b) = enforcer_b
.enforce_query("shared_data", &["name".to_string()])
.expect("tenant B can access its shared_data");
assert_eq!(allowed_a, vec!["name".to_string()]);
assert_eq!(allowed_b, vec!["name".to_string()]);
assert!(where_a != where_b);
assert!(where_a.contains(&u64::from(tenant_a).to_string()));
assert!(where_b.contains(&u64::from(tenant_b).to_string()));
assert_eq!(policy_a.tenant_id, Some(tenant_a));
assert_eq!(policy_b.tenant_id, Some(tenant_b));
assert_ne!(policy_a.tenant_id, policy_b.tenant_id);
}
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(5)]
fn verify_role_binding_per_tenant() {
let a_raw: u64 = kani::any();
let b_raw: u64 = kani::any();
kani::assume(a_raw != b_raw);
let tenant_a = TenantId::new(a_raw);
let tenant_b = TenantId::new(b_raw);
let admin_policy_a = AccessPolicy::new(Role::Admin)
.with_tenant(tenant_a)
.allow_stream("shared_stream")
.allow_column("public_col")
.allow_column("sensitive_col")
.with_row_filter(RowFilter::new(
"tenant_id",
RowFilterOperator::Eq,
u64::from(tenant_a).to_string(),
));
let user_policy_b = AccessPolicy::new(Role::User)
.with_tenant(tenant_b)
.allow_stream("shared_stream")
.allow_column("public_col")
.deny_column("sensitive_col")
.with_row_filter(RowFilter::new(
"tenant_id",
RowFilterOperator::Eq,
u64::from(tenant_b).to_string(),
));
let enforcer_a = PolicyEnforcer::new(admin_policy_a).without_audit();
let enforcer_b = PolicyEnforcer::new(user_policy_b).without_audit();
let (cols_a, where_a) = enforcer_a
.enforce_query(
"shared_stream",
&["public_col".to_string(), "sensitive_col".to_string()],
)
.expect("tenant A Admin access");
assert!(cols_a.iter().any(|c| c == "sensitive_col"));
assert!(where_a.contains(&u64::from(tenant_a).to_string()));
let b_result = enforcer_b.enforce_query(
"shared_stream",
&["public_col".to_string(), "sensitive_col".to_string()],
);
if let Ok((cols_b, where_b)) = b_result {
assert!(
!cols_b.iter().any(|c| c == "sensitive_col"),
"tenant B must not see sensitive_col from tenant A's Admin policy"
);
assert!(where_b.contains(&u64::from(tenant_b).to_string()));
assert!(!where_b.contains(&u64::from(tenant_a).to_string()));
}
}
#[cfg(kani)]
#[kani::proof]
#[kani::unwind(3)]
fn verify_role_restrictiveness() {
assert_eq!(Role::Admin.restrictiveness(), 0);
assert_eq!(Role::Auditor.restrictiveness(), 3);
assert!(Role::Admin.restrictiveness() < Role::Analyst.restrictiveness());
assert!(Role::Analyst.restrictiveness() < Role::User.restrictiveness());
assert!(Role::User.restrictiveness() < Role::Auditor.restrictiveness());
assert!(!Role::User.can_escalate_to(Role::Admin));
assert!(Role::Admin.can_escalate_to(Role::User));
}