use crate::helpers::{TENANT_A, TENANT_B};
use nodedb::control::security::audit::NoopAuditEmitter;
use nodedb::control::security::auth_context::AuthContext;
use nodedb::control::security::identity::{AuthMethod, AuthenticatedIdentity, Role};
use nodedb::control::security::predicate::{CompareOp, PolicyMode, PredicateValue, RlsPredicate};
use nodedb::control::security::rls::{PolicyType, RlsPolicy, RlsPolicyStore};
use nodedb_types::TenantId;
const NOOP: &NoopAuditEmitter = &NoopAuditEmitter;
fn make_auth(tenant_id: u64) -> AuthContext {
let identity = AuthenticatedIdentity {
user_id: 1,
username: "user1".into(),
tenant_id: TenantId::new(tenant_id),
auth_method: AuthMethod::ApiKey,
roles: vec![Role::ReadWrite],
is_superuser: false,
default_database: None,
accessible_databases: AuthenticatedIdentity::default_database_set(false),
};
AuthContext::from_identity(&identity, "test".into())
}
fn status_policy(tenant_id: u64, name: &str) -> RlsPolicy {
RlsPolicy {
name: name.into(),
collection: "orders".into(),
tenant_id,
policy_type: PolicyType::Write,
compiled_predicate: Some(RlsPredicate::Compare {
field: "status".into(),
op: CompareOp::Eq,
value: PredicateValue::Literal(serde_json::json!("approved")),
}),
mode: PolicyMode::default(),
on_deny: Default::default(),
enabled: true,
created_by: "admin".into(),
created_at: 0,
}
}
#[test]
fn rls_tenant_b_cannot_list_tenant_a_policies() {
let store = RlsPolicyStore::new();
store
.create_policy(status_policy(TENANT_A, "a_policy"))
.unwrap();
let policies_b = store.all_policies(TENANT_B, "orders");
assert!(
policies_b.is_empty(),
"Tenant B must not see Tenant A's RLS policies; got {} policies",
policies_b.len()
);
}
#[test]
fn rls_same_name_policy_in_different_tenants_is_isolated() {
let store = RlsPolicyStore::new();
store
.create_policy(status_policy(TENANT_A, "shared_name"))
.unwrap();
store
.create_policy(RlsPolicy {
name: "shared_name".into(),
collection: "orders".into(),
tenant_id: TENANT_B,
policy_type: PolicyType::Read,
compiled_predicate: None,
mode: PolicyMode::default(),
on_deny: Default::default(),
enabled: true,
created_by: "admin".into(),
created_at: 0,
})
.unwrap();
let policies_a = store.all_policies(TENANT_A, "orders");
assert_eq!(policies_a.len(), 1);
assert_eq!(policies_a[0].policy_type, PolicyType::Write);
let policies_b = store.all_policies(TENANT_B, "orders");
assert_eq!(policies_b.len(), 1);
assert_eq!(policies_b[0].policy_type, PolicyType::Read);
}
#[test]
fn rls_policies_enforce_independently_per_tenant() {
let store = RlsPolicyStore::new();
store
.create_policy(status_policy(TENANT_A, "a_gate"))
.unwrap();
let pending = serde_json::json!({"status": "pending", "amount": 50});
let approved = serde_json::json!({"status": "approved", "amount": 50});
assert!(
store
.check_write_with_auth(TENANT_A, "orders", &pending, &make_auth(TENANT_A), NOOP)
.is_err(),
"Tenant A's RLS must block pending write"
);
assert!(
store
.check_write_with_auth(TENANT_B, "orders", &pending, &make_auth(TENANT_B), NOOP)
.is_ok(),
"Tenant B must be unrestricted before its own policy is created"
);
store
.create_policy(status_policy(TENANT_B, "b_gate"))
.unwrap();
assert!(
store
.check_write_with_auth(TENANT_B, "orders", &pending, &make_auth(TENANT_B), NOOP)
.is_err(),
"Tenant B's own RLS must block pending write after b_gate is created"
);
assert!(
store
.check_write_with_auth(TENANT_A, "orders", &pending, &make_auth(TENANT_A), NOOP)
.is_err(),
"Tenant A's RLS must still block pending write"
);
assert!(
store
.check_write_with_auth(TENANT_A, "orders", &approved, &make_auth(TENANT_A), NOOP)
.is_ok()
);
assert!(
store
.check_write_with_auth(TENANT_B, "orders", &approved, &make_auth(TENANT_B), NOOP)
.is_ok()
);
}