use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct PermissionContext {
pub user_id: Option<i64>,
pub tenant_id: Option<i64>,
pub dept_id: Option<i64>,
pub roles: Vec<String>,
pub permissions: Vec<String>,
pub extras: HashMap<String, String>,
}
impl PermissionContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_user_id(mut self, user_id: i64) -> Self {
self.user_id = Some(user_id);
self
}
pub fn with_tenant_id(mut self, tenant_id: i64) -> Self {
self.tenant_id = Some(tenant_id);
self
}
pub fn with_dept_id(mut self, dept_id: i64) -> Self {
self.dept_id = Some(dept_id);
self
}
pub fn with_roles(mut self, roles: Vec<String>) -> Self {
self.roles = roles;
self
}
pub fn with_permissions(mut self, perms: Vec<String>) -> Self {
self.permissions = perms;
self
}
pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extras.insert(key.into(), value.into());
self
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn has_permission(&self, perm: &str) -> bool {
self.permissions.iter().any(|p| p == perm)
}
pub fn is_admin(&self) -> bool {
self.has_role("admin") || self.has_role("super_admin")
}
}
pub trait PermissionRule: Send + Sync {
fn name(&self) -> &'static str;
fn apply(&self, ctx: &PermissionContext) -> Result<Option<String>, PermissionError>;
}
#[derive(Debug)]
pub enum PermissionError {
MissingContext {
field: &'static str,
},
ConfigError(String),
Forbidden(String),
}
impl std::fmt::Display for PermissionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PermissionError::MissingContext { field } => {
write!(f, "Permission context missing field: `{}`", field)
}
PermissionError::ConfigError(msg) => {
write!(f, "Permission rule config error: {}", msg)
}
PermissionError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
}
}
}
impl std::error::Error for PermissionError {}
pub type PermissionResult<T> = Result<T, PermissionError>;
pub struct TenantIsolation {
pub field: &'static str,
}
impl TenantIsolation {
pub fn new(field: &'static str) -> Self {
Self { field }
}
pub fn default_field() -> Self {
Self::new("tenant_id")
}
}
impl PermissionRule for TenantIsolation {
fn name(&self) -> &'static str {
"TenantIsolation"
}
fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
if ctx.is_admin() {
return Ok(None);
}
match ctx.tenant_id {
Some(tid) => Ok(Some(format!("{} = {}", self.field, tid))),
None => Err(PermissionError::MissingContext { field: "tenant_id" }),
}
}
}
pub struct OwnerOnly {
pub field: &'static str,
}
impl OwnerOnly {
pub fn new(field: &'static str) -> Self {
Self { field }
}
pub fn default_field() -> Self {
Self::new("user_id")
}
}
impl PermissionRule for OwnerOnly {
fn name(&self) -> &'static str {
"OwnerOnly"
}
fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
if ctx.is_admin() {
return Ok(None);
}
match ctx.user_id {
Some(uid) => Ok(Some(format!("{} = {}", self.field, uid))),
None => Err(PermissionError::MissingContext { field: "user_id" }),
}
}
}
pub struct DepartmentScope {
pub field: &'static str,
pub include_sub_depts: Vec<i64>,
}
impl DepartmentScope {
pub fn new(field: &'static str) -> Self {
Self {
field,
include_sub_depts: Vec::new(),
}
}
pub fn default_field() -> Self {
Self::new("dept_id")
}
pub fn with_sub_depts(mut self, depts: Vec<i64>) -> Self {
self.include_sub_depts = depts;
self
}
}
impl PermissionRule for DepartmentScope {
fn name(&self) -> &'static str {
"DepartmentScope"
}
fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
if ctx.is_admin() {
return Ok(None);
}
match ctx.dept_id {
Some(did) => {
if self.include_sub_depts.is_empty() {
Ok(Some(format!("{} = {}", self.field, did)))
} else {
let mut all_depts = vec![did];
all_depts.extend(self.include_sub_depts.iter().copied());
let list = all_depts
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join(", ");
Ok(Some(format!("{} IN ({})", self.field, list)))
}
}
None => Err(PermissionError::MissingContext { field: "dept_id" }),
}
}
}
pub type ConditionGenerator = Box<dyn Fn(&PermissionContext) -> Option<String> + Send + Sync>;
pub struct CustomCondition {
pub name_str: &'static str,
pub generator: ConditionGenerator,
}
impl CustomCondition {
pub fn new(
name: &'static str,
generator: impl Fn(&PermissionContext) -> Option<String> + Send + Sync + 'static,
) -> Self {
Self {
name_str: name,
generator: Box::new(generator),
}
}
}
impl PermissionRule for CustomCondition {
fn name(&self) -> &'static str {
self.name_str
}
fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
Ok((self.generator)(ctx))
}
}
pub struct DataPermissionInterceptor {
rules: Vec<Box<dyn PermissionRule>>,
}
impl DataPermissionInterceptor {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn register(&mut self, rule: Box<dyn PermissionRule>) {
self.rules.push(rule);
}
pub fn count(&self) -> usize {
self.rules.len()
}
pub fn names(&self) -> Vec<&'static str> {
self.rules.iter().map(|r| r.name()).collect()
}
pub fn collect_clauses(&self, ctx: &PermissionContext) -> PermissionResult<Vec<String>> {
let mut clauses = Vec::new();
for rule in &self.rules {
if let Some(clause) = rule.apply(ctx)? {
if !clause.trim().is_empty() {
clauses.push(clause);
}
}
}
Ok(clauses)
}
pub fn apply_to_select(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
let clauses = self.collect_clauses(ctx)?;
if clauses.is_empty() {
return Ok(sql.to_string());
}
Ok(append_where_clauses(sql, &clauses))
}
pub fn apply_to_update(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
self.apply_to_select(sql, ctx)
}
pub fn apply_to_delete(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
self.apply_to_select(sql, ctx)
}
}
impl Default for DataPermissionInterceptor {
fn default() -> Self {
Self::new()
}
}
pub fn append_where_clauses(sql: &str, clauses: &[String]) -> String {
if clauses.is_empty() {
return sql.to_string();
}
let combined = clauses.join(" AND ");
let upper = sql.to_uppercase();
let where_pos = find_keyword(&upper, "WHERE");
let group_by_pos = find_keyword(&upper, "GROUP BY");
let order_by_pos = find_keyword(&upper, "ORDER BY");
let limit_pos = find_keyword(&upper, "LIMIT");
let having_pos = find_keyword(&upper, "HAVING");
let end_pos = [group_by_pos, order_by_pos, limit_pos, having_pos]
.iter()
.filter_map(|x| *x)
.min();
if let Some(wp) = where_pos {
let insert_pos = end_pos.unwrap_or(sql.len());
let before = &sql[..wp + 5]; let existing_clause = &sql[wp + 5..insert_pos];
let after = &sql[insert_pos..];
let trimmed_existing = existing_clause.trim();
if trimmed_existing.is_empty() {
format!("{} {}{}", before, combined, after)
} else {
format!(
"{} ({} ) AND ({}){}",
before, trimmed_existing, combined, after
)
}
} else {
let insert_pos = end_pos.unwrap_or(sql.len());
let before = &sql[..insert_pos];
let after = &sql[insert_pos..];
let trimmed = before.trim_end();
let sep = if trimmed.is_empty() { "" } else { " " };
format!("{}{}WHERE {}{}", trimmed, sep, combined, after)
}
}
fn find_keyword(sql: &str, keyword: &str) -> Option<usize> {
let upper_sql = sql.to_uppercase();
let kw_upper = keyword.to_uppercase();
let kw_len = kw_upper.len();
if kw_len == 0 || upper_sql.len() < kw_len {
return None;
}
let bytes = upper_sql.as_bytes();
let kw_bytes = kw_upper.as_bytes();
let mut i = 0;
let mut depth: i32 = 0; while i + kw_len <= bytes.len() {
let b = bytes[i];
if b == b'(' {
depth += 1;
i += 1;
continue;
}
if b == b')' {
if depth > 0 {
depth -= 1;
}
i += 1;
continue;
}
if depth == 0 && &bytes[i..i + kw_len] == kw_bytes {
let prev_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
let next_idx = i + kw_len;
let next_ok = next_idx >= bytes.len()
|| !bytes[next_idx].is_ascii_alphanumeric() && bytes[next_idx] != b'_';
if prev_ok && next_ok {
return Some(i);
}
}
i += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_context_builders() {
let ctx = PermissionContext::new()
.with_user_id(100)
.with_tenant_id(5)
.with_dept_id(3)
.with_roles(vec!["user".to_string()])
.with_permissions(vec!["read".to_string()])
.with_extra("region", "cn");
assert_eq!(ctx.user_id, Some(100));
assert_eq!(ctx.tenant_id, Some(5));
assert_eq!(ctx.dept_id, Some(3));
assert!(ctx.has_role("user"));
assert!(!ctx.has_role("admin"));
assert!(ctx.has_permission("read"));
assert_eq!(ctx.extras.get("region"), Some(&"cn".to_string()));
}
#[test]
fn test_permission_context_is_admin() {
let admin_ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
assert!(admin_ctx.is_admin());
let super_admin_ctx = PermissionContext::new().with_roles(vec!["super_admin".to_string()]);
assert!(super_admin_ctx.is_admin());
let user_ctx = PermissionContext::new().with_roles(vec!["user".to_string()]);
assert!(!user_ctx.is_admin());
}
#[test]
fn test_tenant_isolation_applies() {
let rule = TenantIsolation::default_field();
let ctx = PermissionContext::new().with_tenant_id(5);
let clause = rule.apply(&ctx).unwrap().unwrap();
assert_eq!(clause, "tenant_id = 5");
}
#[test]
fn test_tenant_isolation_skips_admin() {
let rule = TenantIsolation::default_field();
let ctx = PermissionContext::new()
.with_tenant_id(5)
.with_roles(vec!["admin".to_string()]);
let clause = rule.apply(&ctx).unwrap();
assert!(clause.is_none());
}
#[test]
fn test_tenant_isolation_missing_context() {
let rule = TenantIsolation::default_field();
let ctx = PermissionContext::new();
let result = rule.apply(&ctx);
assert!(matches!(
result,
Err(PermissionError::MissingContext { field }) if field == "tenant_id"
));
}
#[test]
fn test_tenant_isolation_custom_field() {
let rule = TenantIsolation::new("org_id");
let ctx = PermissionContext::new().with_tenant_id(99);
let clause = rule.apply(&ctx).unwrap().unwrap();
assert_eq!(clause, "org_id = 99");
}
#[test]
fn test_owner_only_applies() {
let rule = OwnerOnly::default_field();
let ctx = PermissionContext::new().with_user_id(100);
let clause = rule.apply(&ctx).unwrap().unwrap();
assert_eq!(clause, "user_id = 100");
}
#[test]
fn test_owner_only_skips_admin() {
let rule = OwnerOnly::default_field();
let ctx = PermissionContext::new()
.with_user_id(100)
.with_roles(vec!["admin".to_string()]);
let clause = rule.apply(&ctx).unwrap();
assert!(clause.is_none());
}
#[test]
fn test_owner_only_missing_context() {
let rule = OwnerOnly::default_field();
let ctx = PermissionContext::new();
let result = rule.apply(&ctx);
assert!(matches!(
result,
Err(PermissionError::MissingContext { field }) if field == "user_id"
));
}
#[test]
fn test_department_scope_simple() {
let rule = DepartmentScope::default_field();
let ctx = PermissionContext::new().with_dept_id(3);
let clause = rule.apply(&ctx).unwrap().unwrap();
assert_eq!(clause, "dept_id = 3");
}
#[test]
fn test_department_scope_with_sub_depts() {
let rule = DepartmentScope::default_field().with_sub_depts(vec![10, 11, 12]);
let ctx = PermissionContext::new().with_dept_id(3);
let clause = rule.apply(&ctx).unwrap().unwrap();
assert_eq!(clause, "dept_id IN (3, 10, 11, 12)");
}
#[test]
fn test_department_scope_skips_admin() {
let rule = DepartmentScope::default_field();
let ctx = PermissionContext::new()
.with_dept_id(3)
.with_roles(vec!["admin".to_string()]);
let clause = rule.apply(&ctx).unwrap();
assert!(clause.is_none());
}
#[test]
fn test_custom_condition_returns_clause() {
let rule = CustomCondition::new("draft_filter", |ctx| {
if ctx.is_admin() {
None
} else {
Some("status != 'draft'".to_string())
}
});
let ctx = PermissionContext::new().with_user_id(1);
let clause = rule.apply(&ctx).unwrap().unwrap();
assert_eq!(clause, "status != 'draft'");
}
#[test]
fn test_custom_condition_skips_admin() {
let rule = CustomCondition::new("draft_filter", |ctx| {
if ctx.is_admin() {
None
} else {
Some("status != 'draft'".to_string())
}
});
let ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
let clause = rule.apply(&ctx).unwrap();
assert!(clause.is_none());
}
#[test]
fn test_interceptor_no_rules_returns_original_sql() {
let interceptor = DataPermissionInterceptor::new();
let ctx = PermissionContext::new().with_user_id(1);
let sql = interceptor
.apply_to_select("SELECT * FROM users", &ctx)
.unwrap();
assert_eq!(sql, "SELECT * FROM users");
}
#[test]
fn test_interceptor_single_rule_no_where() {
let mut interceptor = DataPermissionInterceptor::new();
interceptor.register(Box::new(TenantIsolation::default_field()));
let ctx = PermissionContext::new().with_tenant_id(5);
let sql = interceptor
.apply_to_select("SELECT * FROM orders", &ctx)
.unwrap();
assert!(sql.contains("WHERE tenant_id = 5"));
}
#[test]
fn test_interceptor_multiple_rules_no_where() {
let mut interceptor = DataPermissionInterceptor::new();
interceptor.register(Box::new(TenantIsolation::default_field()));
interceptor.register(Box::new(OwnerOnly::default_field()));
let ctx = PermissionContext::new().with_tenant_id(5).with_user_id(100);
let sql = interceptor
.apply_to_select("SELECT * FROM orders", &ctx)
.unwrap();
assert!(sql.contains("tenant_id = 5"));
assert!(sql.contains("user_id = 100"));
assert!(sql.contains("AND"));
}
#[test]
fn test_interceptor_appends_to_existing_where() {
let mut interceptor = DataPermissionInterceptor::new();
interceptor.register(Box::new(TenantIsolation::default_field()));
let ctx = PermissionContext::new().with_tenant_id(5);
let sql = interceptor
.apply_to_select("SELECT * FROM orders WHERE status = 'active'", &ctx)
.unwrap();
assert!(sql.contains("status = 'active'"));
assert!(sql.contains("tenant_id = 5"));
assert!(sql.contains("AND"));
}
#[test]
fn test_interceptor_admin_skips_all_rules() {
let mut interceptor = DataPermissionInterceptor::new();
interceptor.register(Box::new(TenantIsolation::default_field()));
interceptor.register(Box::new(OwnerOnly::default_field()));
let ctx = PermissionContext::new()
.with_tenant_id(5)
.with_user_id(100)
.with_roles(vec!["admin".to_string()]);
let sql = interceptor
.apply_to_select("SELECT * FROM orders", &ctx)
.unwrap();
assert_eq!(sql, "SELECT * FROM orders");
}
#[test]
fn test_interceptor_apply_to_update() {
let mut interceptor = DataPermissionInterceptor::new();
interceptor.register(Box::new(TenantIsolation::default_field()));
let ctx = PermissionContext::new().with_tenant_id(5);
let sql = interceptor
.apply_to_update("UPDATE orders SET status = 'shipped' WHERE id = 1", &ctx)
.unwrap();
assert!(sql.contains("id = 1"));
assert!(sql.contains("tenant_id = 5"));
}
#[test]
fn test_interceptor_apply_to_delete() {
let mut interceptor = DataPermissionInterceptor::new();
interceptor.register(Box::new(OwnerOnly::default_field()));
let ctx = PermissionContext::new().with_user_id(100);
let sql = interceptor
.apply_to_delete("DELETE FROM orders WHERE id = 1", &ctx)
.unwrap();
assert!(sql.contains("id = 1"));
assert!(sql.contains("user_id = 100"));
}
#[test]
fn test_interceptor_count_and_names() {
let mut interceptor = DataPermissionInterceptor::new();
assert_eq!(interceptor.count(), 0);
interceptor.register(Box::new(TenantIsolation::default_field()));
interceptor.register(Box::new(OwnerOnly::default_field()));
assert_eq!(interceptor.count(), 2);
let names = interceptor.names();
assert!(names.contains(&"TenantIsolation"));
assert!(names.contains(&"OwnerOnly"));
}
#[test]
fn test_append_where_no_existing_where_no_clauses() {
let sql = append_where_clauses("SELECT * FROM users", &[]);
assert_eq!(sql, "SELECT * FROM users");
}
#[test]
fn test_append_where_no_existing_where_with_clauses() {
let sql = append_where_clauses("SELECT * FROM users", &["tenant_id = 5".to_string()]);
assert_eq!(sql, "SELECT * FROM users WHERE tenant_id = 5");
}
#[test]
fn test_append_where_existing_where_with_clauses() {
let sql = append_where_clauses(
"SELECT * FROM users WHERE id = 1",
&["tenant_id = 5".to_string()],
);
assert!(sql.contains("id = 1"));
assert!(sql.contains("tenant_id = 5"));
assert!(sql.contains("AND"));
}
#[test]
fn test_append_where_inserts_before_group_by() {
let sql = append_where_clauses(
"SELECT * FROM users GROUP BY dept_id",
&["tenant_id = 5".to_string()],
);
let where_idx = sql.to_uppercase().find("WHERE").unwrap();
let group_by_idx = sql.to_uppercase().find("GROUP BY").unwrap();
assert!(where_idx < group_by_idx);
}
#[test]
fn test_append_where_inserts_before_order_by() {
let sql = append_where_clauses(
"SELECT * FROM users ORDER BY id",
&["tenant_id = 5".to_string()],
);
let where_idx = sql.to_uppercase().find("WHERE").unwrap();
let order_by_idx = sql.to_uppercase().find("ORDER BY").unwrap();
assert!(where_idx < order_by_idx);
}
#[test]
fn test_append_where_inserts_before_limit() {
let sql = append_where_clauses(
"SELECT * FROM users LIMIT 10",
&["tenant_id = 5".to_string()],
);
let where_idx = sql.to_uppercase().find("WHERE").unwrap();
let limit_idx = sql.to_uppercase().find("LIMIT").unwrap();
assert!(where_idx < limit_idx);
}
#[test]
fn test_permission_error_display_missing_context() {
let e = PermissionError::MissingContext { field: "user_id" };
let s = format!("{}", e);
assert!(s.contains("user_id"));
assert!(s.contains("missing"));
}
#[test]
fn test_permission_error_display_config_error() {
let e = PermissionError::ConfigError("invalid rule".to_string());
let s = format!("{}", e);
assert!(s.contains("invalid rule"));
}
#[test]
fn test_permission_error_display_forbidden() {
let e = PermissionError::Forbidden("cross-tenant access".to_string());
let s = format!("{}", e);
assert!(s.contains("Forbidden"));
assert!(s.contains("cross-tenant access"));
}
#[test]
fn test_find_keyword_basic() {
assert_eq!(
find_keyword("SELECT * FROM users WHERE id = 1", "WHERE"),
Some(20)
);
}
#[test]
fn test_find_keyword_not_found() {
assert_eq!(find_keyword("SELECT * FROM users", "WHERE"), None);
}
#[test]
fn test_find_keyword_word_boundary() {
assert_eq!(find_keyword("SELECT somewhere FROM t", "WHERE"), None);
}
#[test]
fn test_find_keyword_case_insensitive() {
assert_eq!(
find_keyword("select * from t where id = 1", "WHERE"),
Some(16)
);
}
#[test]
fn test_find_keyword_skips_subquery_where() {
let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs WHERE level = 1)";
let pos = find_keyword(sql, "WHERE").unwrap();
assert_eq!(pos, 20);
assert_eq!(&sql[pos..pos + 5], "WHERE");
}
#[test]
fn test_find_keyword_skips_subquery_limit() {
let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5)";
assert_eq!(find_keyword(sql, "LIMIT"), None);
}
#[test]
fn test_find_keyword_finds_outer_limit() {
let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5) LIMIT 10";
let pos = find_keyword(sql, "LIMIT").unwrap();
assert_eq!(&sql[pos..pos + 5], "LIMIT");
assert!(pos > 50, "should match outer LIMIT, got pos={}", pos);
}
#[test]
fn test_find_keyword_skips_nested_parentheses() {
let sql = "SELECT * FROM t WHERE id IN (SELECT id FROM (SELECT * FROM t2 WHERE x = 1) sub)";
let pos = find_keyword(sql, "WHERE").unwrap();
assert_eq!(&sql[pos..pos + 5], "WHERE");
assert_eq!(pos, 16);
}
#[test]
fn test_find_keyword_unbalanced_parentheses_safe() {
let sql = "SELECT * FROM t WHERE id = 1)";
assert!(find_keyword(sql, "WHERE").is_some());
}
#[test]
fn test_interceptor_default_is_empty() {
let i = DataPermissionInterceptor::default();
assert_eq!(i.count(), 0);
}
}