use proptest::prelude::*;
use nodedb::control::security::auth_context::{AuthContext, AuthStatus};
use nodedb::control::security::identity::{AuthMethod, AuthenticatedIdentity, Role};
use nodedb::control::security::predicate::{CompareOp, PredicateValue, RlsPredicate};
use nodedb::control::security::predicate_eval::substitute_to_scan_filters;
use nodedb::types::TenantId;
fn arb_auth_context() -> impl Strategy<Value = AuthContext> {
(
"[a-z]{3,8}", "[a-z]{3,8}", proptest::option::of("[a-z]+@[a-z]+\\.[a-z]+"), 1u32..100, proptest::collection::vec("[a-z]{3,8}".prop_map(String::from), 0..5), proptest::collection::vec("[a-z]{3,8}".prop_map(String::from), 0..3), )
.prop_map(|(id, username, email, tid, roles, groups)| {
let identity = AuthenticatedIdentity {
user_id: 1,
username: username.clone(),
tenant_id: TenantId::new(tid),
auth_method: AuthMethod::Trust,
roles: vec![Role::ReadWrite],
is_superuser: false,
};
let mut ctx = AuthContext::from_identity(&identity, "fuzz".into());
ctx.id = id;
ctx.username = username;
ctx.email = email;
ctx.roles = roles;
ctx.groups = groups;
ctx.status = AuthStatus::Active;
ctx
})
}
fn arb_predicate() -> impl Strategy<Value = RlsPredicate> {
prop_oneof![
Just(RlsPredicate::AlwaysTrue),
Just(RlsPredicate::AlwaysFalse),
"[a-z_]{3,10}".prop_map(|field| {
RlsPredicate::Compare {
field,
op: CompareOp::Eq,
value: PredicateValue::AuthRef("id".into()),
}
}),
("[a-z_]{3,10}", "[a-z]{3,8}").prop_map(|(field, val)| {
RlsPredicate::Compare {
field,
op: CompareOp::Eq,
value: PredicateValue::Literal(serde_json::json!(val)),
}
}),
"[a-z]{3,8}".prop_map(|role| {
RlsPredicate::Contains {
set: PredicateValue::AuthRef("roles".into()),
element: PredicateValue::Literal(serde_json::json!(role)),
}
}),
]
}
proptest! {
#[test]
fn substitution_never_panics(
auth in arb_auth_context(),
pred in arb_predicate(),
) {
let _ = substitute_to_scan_filters(&pred, &auth);
}
#[test]
fn always_true_produces_match_all(auth in arb_auth_context()) {
let filters = substitute_to_scan_filters(&RlsPredicate::AlwaysTrue, &auth).unwrap();
prop_assert_eq!(filters.len(), 1);
prop_assert_eq!(filters[0].op.as_str(), "match_all");
}
#[test]
fn always_false_produces_deny(auth in arb_auth_context()) {
let filters = substitute_to_scan_filters(&RlsPredicate::AlwaysFalse, &auth).unwrap();
prop_assert_eq!(filters.len(), 1);
prop_assert_eq!(filters[0].field.as_str(), "__rls_deny__");
}
#[test]
fn deny_filter_rejects_all_docs(
auth in arb_auth_context(),
doc_field in "[a-z_]{3,10}",
doc_val in "[a-z]{3,8}",
) {
let deny_filters = substitute_to_scan_filters(&RlsPredicate::AlwaysFalse, &auth).unwrap();
let doc = serde_json::json!({ doc_field: doc_val });
let passes = deny_filters.iter().all(|f| f.matches(&doc));
prop_assert!(!passes, "deny filter should reject all documents");
}
#[test]
fn contains_matching_role_allows(
mut auth in arb_auth_context(),
) {
if auth.roles.is_empty() {
auth.roles.push("testrole".into());
}
let role = auth.roles[0].clone();
let pred = RlsPredicate::Contains {
set: PredicateValue::AuthRef("roles".into()),
element: PredicateValue::Literal(serde_json::json!(role)),
};
let filters = substitute_to_scan_filters(&pred, &auth).unwrap();
prop_assert_eq!(filters.len(), 1);
prop_assert_eq!(filters[0].op.as_str(), "match_all");
}
}