use crate::value::Value;
#[derive(Debug, Clone)]
pub struct ParameterizedCondition {
pub sql_fragment: String,
pub params: Vec<Value>,
}
impl ParameterizedCondition {
pub fn new(sql_fragment: impl Into<String>, params: Vec<Value>) -> Self {
Self {
sql_fragment: sql_fragment.into(),
params,
}
}
pub fn literal(sql_fragment: impl Into<String>) -> Self {
Self {
sql_fragment: sql_fragment.into(),
params: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct Principal {
pub tenant_id: i64,
pub roles: Vec<String>,
}
impl Principal {
pub fn new(tenant_id: i64, roles: Vec<String>) -> Self {
Self { tenant_id, roles }
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
}
#[derive(Debug, Clone)]
pub struct RowLevelSecurityPolicy {
pub table: String,
pub filter_condition: ParameterizedCondition,
pub principal: Principal,
}
impl RowLevelSecurityPolicy {
pub fn new(
table: impl Into<String>,
filter_condition: ParameterizedCondition,
principal: Principal,
) -> Self {
Self {
table: table.into(),
filter_condition,
principal,
}
}
}
pub use sz_orm_masking::MaskingRule as MaskingFunction;
#[derive(Debug, Clone)]
pub struct PermissionPredicate {
pub applicable_roles: Option<Vec<String>>,
pub exempt_roles: Vec<String>,
}
impl PermissionPredicate {
pub fn all() -> Self {
Self {
applicable_roles: None,
exempt_roles: Vec::new(),
}
}
pub fn for_roles(roles: Vec<String>) -> Self {
Self {
applicable_roles: Some(roles),
exempt_roles: Vec::new(),
}
}
pub fn with_exempt(mut self, roles: Vec<String>) -> Self {
self.exempt_roles = roles;
self
}
pub fn applies_to(&self, roles: &[String]) -> bool {
if roles.iter().any(|r| self.exempt_roles.contains(r)) {
return false;
}
match &self.applicable_roles {
None => true,
Some(applicable) => roles.iter().any(|r| applicable.contains(r)),
}
}
}
impl Default for PermissionPredicate {
fn default() -> Self {
Self::all()
}
}
#[derive(Debug, Clone)]
pub struct ColumnMaskingRule {
pub table: String,
pub column: String,
pub masking_function: MaskingFunction,
pub applicable_permissions: PermissionPredicate,
}
impl ColumnMaskingRule {
pub fn new(
table: impl Into<String>,
column: impl Into<String>,
masking_function: MaskingFunction,
applicable_permissions: PermissionPredicate,
) -> Self {
Self {
table: table.into(),
column: column.into(),
masking_function,
applicable_permissions,
}
}
pub fn mask(&self, value: &str) -> String {
sz_orm_masking::DataMasker::apply(&self.masking_function, value)
}
pub fn applies_to(&self, roles: &[String]) -> bool {
self.applicable_permissions.applies_to(roles)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TenantAuditOperation {
ContextSet,
ContextSwitch,
CrossTenantDenied,
RowLevelFiltered,
ColumnMasked,
}
impl std::fmt::Display for TenantAuditOperation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ContextSet => write!(f, "context_set"),
Self::ContextSwitch => write!(f, "context_switch"),
Self::CrossTenantDenied => write!(f, "cross_tenant_denied"),
Self::RowLevelFiltered => write!(f, "row_level_filtered"),
Self::ColumnMasked => write!(f, "column_masked"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuditResult {
Success,
Denied,
}
impl std::fmt::Display for AuditResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Success => write!(f, "success"),
Self::Denied => write!(f, "denied"),
}
}
}
#[derive(Debug, Clone)]
pub struct TenantAuditContext {
pub tenant_id: i64,
pub operation: TenantAuditOperation,
pub timestamp: i64,
pub result: AuditResult,
pub detail: String,
}
impl TenantAuditContext {
pub fn new(
tenant_id: i64,
operation: TenantAuditOperation,
result: AuditResult,
detail: impl Into<String>,
) -> Self {
Self {
tenant_id,
operation,
timestamp: chrono::Utc::now().timestamp(),
result,
detail: detail.into(),
}
}
pub fn log_to(&self, auditor: &sz_orm_audit::SqlAuditor) {
let ctx = sz_orm_audit::SqlAuditContext {
sql: format!(
"[tenant={}] {} {} {}",
self.tenant_id, self.operation, self.result, self.detail
),
user: format!("tenant_{}", self.tenant_id),
timestamp: self.timestamp,
};
auditor.log(&ctx);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parameterized_condition_new() {
let cond = ParameterizedCondition::new("department_id = $1", vec![Value::I64(10)]);
assert_eq!(cond.sql_fragment, "department_id = $1");
assert_eq!(cond.params.len(), 1);
}
#[test]
fn test_parameterized_condition_literal() {
let cond = ParameterizedCondition::literal("is_active = true");
assert_eq!(cond.sql_fragment, "is_active = true");
assert!(cond.params.is_empty());
}
#[test]
fn test_principal_has_role() {
let principal = Principal::new(42, vec!["admin".to_string(), "manager".to_string()]);
assert!(principal.has_role("admin"));
assert!(principal.has_role("manager"));
assert!(!principal.has_role("employee"));
}
#[test]
fn test_row_level_security_policy() {
let policy = RowLevelSecurityPolicy::new(
"orders",
ParameterizedCondition::new("department_id = $1", vec![Value::I64(10)]),
Principal::new(42, vec!["manager".to_string()]),
);
assert_eq!(policy.table, "orders");
assert_eq!(policy.filter_condition.sql_fragment, "department_id = $1");
assert_eq!(policy.principal.tenant_id, 42);
}
#[test]
fn test_permission_predicate_all() {
let pred = PermissionPredicate::all();
let roles = vec!["employee".to_string()];
assert!(pred.applies_to(&roles));
}
#[test]
fn test_permission_predicate_exempt() {
let pred = PermissionPredicate::all().with_exempt(vec!["admin".to_string()]);
let admin_roles = vec!["admin".to_string()];
let employee_roles = vec!["employee".to_string()];
assert!(!pred.applies_to(&admin_roles));
assert!(pred.applies_to(&employee_roles));
}
#[test]
fn test_permission_predicate_for_roles() {
let pred = PermissionPredicate::for_roles(vec!["employee".to_string()]);
let employee_roles = vec!["employee".to_string()];
let manager_roles = vec!["manager".to_string()];
assert!(pred.applies_to(&employee_roles));
assert!(!pred.applies_to(&manager_roles));
}
#[test]
fn test_column_masking_rule_mask() {
let rule = ColumnMaskingRule::new(
"users",
"phone",
MaskingFunction::Phone,
PermissionPredicate::all(),
);
let masked = rule.mask("13812345678");
assert!(masked.starts_with("138"));
assert!(masked.ends_with("5678"));
assert!(masked.contains('*'));
}
#[test]
fn test_column_masking_rule_applies_to() {
let rule = ColumnMaskingRule::new(
"users",
"phone",
MaskingFunction::Phone,
PermissionPredicate::all().with_exempt(vec!["admin".to_string()]),
);
assert!(!rule.applies_to(&["admin".to_string()]));
assert!(rule.applies_to(&["employee".to_string()]));
}
#[test]
fn test_tenant_audit_operation_display() {
assert_eq!(TenantAuditOperation::ContextSet.to_string(), "context_set");
assert_eq!(
TenantAuditOperation::CrossTenantDenied.to_string(),
"cross_tenant_denied"
);
}
#[test]
fn test_audit_result_display() {
assert_eq!(AuditResult::Success.to_string(), "success");
assert_eq!(AuditResult::Denied.to_string(), "denied");
}
#[test]
fn test_tenant_audit_context_new() {
let ctx = TenantAuditContext::new(
42,
TenantAuditOperation::ContextSet,
AuditResult::Success,
"tenant context set for tenant 42",
);
assert_eq!(ctx.tenant_id, 42);
assert_eq!(ctx.operation, TenantAuditOperation::ContextSet);
assert_eq!(ctx.result, AuditResult::Success);
}
#[test]
fn test_tenant_audit_context_log_to() {
let auditor = sz_orm_audit::SqlAuditor::new();
let ctx = TenantAuditContext::new(
42,
TenantAuditOperation::ContextSet,
AuditResult::Success,
"test",
);
ctx.log_to(&auditor);
let logs = auditor.get_logs();
assert_eq!(logs.len(), 1);
assert!(logs[0].sql.contains("tenant=42"));
assert!(logs[0].sql.contains("context_set"));
}
}