use crate::SecurityContext;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DataScopeType {
All,
Custom,
Department,
DeptAndSub,
SelfOnly,
}
impl DataScopeType {
pub fn requires_filtering(&self) -> bool {
!matches!(self, DataScopeType::All)
}
}
impl std::fmt::Display for DataScopeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DataScopeType::All => write!(f, "ALL"),
DataScopeType::Custom => write!(f, "CUSTOM"),
DataScopeType::Department => write!(f, "DEPARTMENT"),
DataScopeType::DeptAndSub => write!(f, "DEPT_AND_SUB"),
DataScopeType::SelfOnly => write!(f, "SELF_ONLY"),
}
}
}
#[derive(Clone, Debug)]
pub struct DataScope {
pub scope_type: DataScopeType,
pub dept_ids: Vec<u64>,
pub custom_conditions: Vec<String>,
}
impl DataScope {
pub fn new(scope_type: DataScopeType) -> Self {
Self {
scope_type,
dept_ids: Vec::new(),
custom_conditions: Vec::new(),
}
}
pub fn all() -> Self {
Self::new(DataScopeType::All)
}
pub fn department(dept_id: u64) -> Self {
Self::new(DataScopeType::Department).with_dept_ids(vec![dept_id])
}
pub fn dept_and_sub(dept_ids: Vec<u64>) -> Self {
Self::new(DataScopeType::DeptAndSub).with_dept_ids(dept_ids)
}
pub fn self_only() -> Self {
Self::new(DataScopeType::SelfOnly)
}
pub fn custom(conditions: Vec<String>) -> Self {
Self::new(DataScopeType::Custom).with_custom_conditions(conditions)
}
pub fn with_dept_ids(mut self, ids: Vec<u64>) -> Self {
self.dept_ids = ids;
self
}
pub fn add_dept_id(mut self, id: u64) -> Self {
self.dept_ids.push(id);
self
}
pub fn with_custom_conditions(mut self, conditions: Vec<String>) -> Self {
self.custom_conditions = conditions;
self
}
pub fn requires_filtering(&self) -> bool {
self.scope_type.requires_filtering()
}
}
impl Default for DataScope {
fn default() -> Self {
Self::all()
}
}
#[derive(Clone, Debug)]
pub struct DataScopeRule {
pub table_alias: String,
pub dept_column: String,
pub user_column: String,
}
impl DataScopeRule {
pub fn new(
table_alias: impl Into<String>,
dept_column: impl Into<String>,
user_column: impl Into<String>,
) -> Self {
Self {
table_alias: table_alias.into(),
dept_column: dept_column.into(),
user_column: user_column.into(),
}
}
pub fn dept_ref(&self) -> String {
format!("{}.{}", self.table_alias, self.dept_column)
}
pub fn user_ref(&self) -> String {
format!("{}.{}", self.table_alias, self.user_column)
}
}
pub trait DataScopeApply {
fn apply_scope(&self, scope: &DataScope, user_id: u64, dept_id: u64) -> String;
}
impl DataScopeApply for DataScopeRule {
fn apply_scope(&self, scope: &DataScope, user_id: u64, dept_id: u64) -> String {
match &scope.scope_type {
DataScopeType::All => String::new(),
DataScopeType::Custom => {
if scope.custom_conditions.is_empty() {
String::new()
} else {
format!("({})", scope.custom_conditions.join(" AND "))
}
},
DataScopeType::Department => {
let col = self.dept_ref();
format!("{} = {}", col, dept_id)
},
DataScopeType::DeptAndSub => {
let col = self.dept_ref();
if scope.dept_ids.is_empty() {
format!("{} = {}", col, dept_id)
} else {
let ids: Vec<String> = scope.dept_ids.iter().map(|id| id.to_string()).collect();
format!("{} IN ({})", col, ids.join(", "))
}
},
DataScopeType::SelfOnly => {
let col = self.user_ref();
format!("{} = {}", col, user_id)
},
}
}
}
#[derive(Clone, Debug)]
pub struct DataScopeContext {
scope: DataScope,
user_id: u64,
dept_id: u64,
}
impl DataScopeContext {
pub fn new(scope: DataScope, user_id: u64, dept_id: u64) -> Self {
Self {
scope,
user_id,
dept_id,
}
}
pub fn scope(&self) -> &DataScope {
&self.scope
}
pub fn user_id(&self) -> u64 {
self.user_id
}
pub fn dept_id(&self) -> u64 {
self.dept_id
}
pub fn apply_rule(&self, rule: &DataScopeRule) -> String {
rule.apply_scope(&self.scope, self.user_id, self.dept_id)
}
}
tokio::task_local! {
static CURRENT_DATA_SCOPE: Arc<RwLock<Option<DataScopeContext>>>;
}
pub fn with_data_scope<F, R>(ctx: DataScopeContext, f: F) -> R
where
F: FnOnce() -> R,
{
CURRENT_DATA_SCOPE.sync_scope(Arc::new(RwLock::new(Some(ctx))), f)
}
pub async fn with_data_scope_async<F, Fut, R>(ctx: DataScopeContext, f: F) -> R
where
F: FnOnce() -> Fut,
Fut: Future<Output = R>,
{
CURRENT_DATA_SCOPE
.scope(Arc::new(RwLock::new(Some(ctx))), f())
.await
}
pub async fn get_data_scope() -> Option<DataScopeContext> {
CURRENT_DATA_SCOPE
.try_with(|lock| {
let guard = lock.try_read().ok()?;
guard.clone()
})
.ok()
.flatten()
}
pub async fn apply_data_scope(rule: &DataScopeRule) -> String {
match get_data_scope().await {
Some(ctx) => ctx.apply_rule(rule),
None => String::new(),
}
}
pub struct DataScopeMiddleware {
scope_resolver:
Arc<dyn Fn(&crate::Authentication) -> Option<(DataScope, u64, u64)> + Send + Sync>,
fallback_user_id: u64,
fallback_dept_id: u64,
}
impl DataScopeMiddleware {
pub fn new<F>(resolver: F) -> Self
where
F: Fn(&crate::Authentication) -> Option<(DataScope, u64, u64)> + Send + Sync + 'static,
{
Self {
scope_resolver: Arc::new(resolver),
fallback_user_id: 0,
fallback_dept_id: 0,
}
}
pub fn fallback_user_id(mut self, id: u64) -> Self {
self.fallback_user_id = id;
self
}
pub fn fallback_dept_id(mut self, id: u64) -> Self {
self.fallback_dept_id = id;
self
}
pub async fn handle<F, Fut, R>(&self, security_context: &SecurityContext, handler: F) -> R
where
F: FnOnce() -> Fut,
Fut: Future<Output = R>,
{
if let Some(auth) = security_context.get_authentication().await {
if let Some((scope, user_id, dept_id)) = (self.scope_resolver)(&auth) {
let ctx = DataScopeContext::new(scope, user_id, dept_id);
return with_data_scope_async(ctx, handler).await;
}
}
handler().await
}
}
pub fn default_scope_resolver()
-> impl Fn(&crate::Authentication) -> Option<(DataScope, u64, u64)> + Send + Sync {
move |auth: &crate::Authentication| {
for authority in &auth.authorities {
if let crate::Authority::Permission(perm) = authority {
if let Some(rest) = perm.strip_prefix("DATASCOPE:") {
return parse_data_scope_authority(rest);
}
}
}
None
}
}
fn parse_data_scope_authority(value: &str) -> Option<(DataScope, u64, u64)> {
let parts: Vec<&str> = value.splitn(4, ':').collect();
if parts.len() < 3 {
return None;
}
let scope_type = match parts[0] {
"ALL" => DataScopeType::All,
"CUSTOM" => DataScopeType::Custom,
"DEPARTMENT" => DataScopeType::Department,
"DEPT_AND_SUB" => DataScopeType::DeptAndSub,
"SELF_ONLY" => DataScopeType::SelfOnly,
_ => return None,
};
let user_id: u64 = parts[1].parse().ok()?;
let dept_id: u64 = parts[2].parse().ok()?;
let mut scope = DataScope::new(scope_type);
if parts.len() == 4 && !parts[3].is_empty() {
let dept_ids: Vec<u64> = parts[3].split(',').filter_map(|s| s.parse().ok()).collect();
scope.dept_ids = dept_ids;
}
Some((scope, user_id, dept_id))
}
pub fn build_data_scope_authority(
scope_type: &DataScopeType,
user_id: u64,
dept_id: u64,
dept_ids: &[u64],
) -> String {
let type_str = scope_type.to_string();
let dept_csv: String = dept_ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(",");
format!("DATASCOPE:{}:{}:{}:{}", type_str, user_id, dept_id, dept_csv)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_scope_type_requires_filtering() {
assert!(!DataScopeType::All.requires_filtering());
assert!(DataScopeType::Custom.requires_filtering());
assert!(DataScopeType::Department.requires_filtering());
assert!(DataScopeType::DeptAndSub.requires_filtering());
assert!(DataScopeType::SelfOnly.requires_filtering());
}
#[test]
fn test_data_scope_type_display() {
assert_eq!(DataScopeType::All.to_string(), "ALL");
assert_eq!(DataScopeType::Custom.to_string(), "CUSTOM");
assert_eq!(DataScopeType::Department.to_string(), "DEPARTMENT");
assert_eq!(DataScopeType::DeptAndSub.to_string(), "DEPT_AND_SUB");
assert_eq!(DataScopeType::SelfOnly.to_string(), "SELF_ONLY");
}
#[test]
fn test_data_scope_constructors() {
let all = DataScope::all();
assert_eq!(all.scope_type, DataScopeType::All);
assert!(all.dept_ids.is_empty());
let dept = DataScope::department(100);
assert_eq!(dept.scope_type, DataScopeType::Department);
assert_eq!(dept.dept_ids, vec![100]);
let sub = DataScope::dept_and_sub(vec![100, 101, 102]);
assert_eq!(sub.scope_type, DataScopeType::DeptAndSub);
assert_eq!(sub.dept_ids, vec![100, 101, 102]);
let self_only = DataScope::self_only();
assert_eq!(self_only.scope_type, DataScopeType::SelfOnly);
let custom = DataScope::custom(vec!["status = 1".to_string()]);
assert_eq!(custom.scope_type, DataScopeType::Custom);
assert_eq!(custom.custom_conditions, vec!["status = 1"]);
}
#[test]
fn test_data_scope_builder() {
let scope = DataScope::new(DataScopeType::DeptAndSub)
.add_dept_id(100)
.add_dept_id(101);
assert_eq!(scope.dept_ids, vec![100, 101]);
}
#[test]
fn test_data_scope_default() {
let scope = DataScope::default();
assert!(!scope.requires_filtering());
}
#[test]
fn test_rule_all_scope() {
let rule = DataScopeRule::new("d", "dept_id", "create_by");
let scope = DataScope::all();
let sql = rule.apply_scope(&scope, 1001, 100);
assert!(sql.is_empty());
}
#[test]
fn test_rule_custom_scope() {
let rule = DataScopeRule::new("d", "dept_id", "create_by");
let scope = DataScope::custom(vec!["status = 1".to_string(), "is_deleted = 0".to_string()]);
let sql = rule.apply_scope(&scope, 1001, 100);
assert_eq!(sql, "(status = 1 AND is_deleted = 0)");
}
#[test]
fn test_rule_custom_scope_empty() {
let rule = DataScopeRule::new("d", "dept_id", "create_by");
let scope = DataScope::custom(vec![]);
let sql = rule.apply_scope(&scope, 1001, 100);
assert!(sql.is_empty());
}
#[test]
fn test_rule_department_scope() {
let rule = DataScopeRule::new("d", "dept_id", "create_by");
let scope = DataScope::department(100);
let sql = rule.apply_scope(&scope, 1001, 100);
assert_eq!(sql, "d.dept_id = 100");
}
#[test]
fn test_rule_dept_and_sub_scope() {
let rule = DataScopeRule::new("d", "dept_id", "create_by");
let scope = DataScope::dept_and_sub(vec![100, 101, 102]);
let sql = rule.apply_scope(&scope, 1001, 100);
assert_eq!(sql, "d.dept_id IN (100, 101, 102)");
}
#[test]
fn test_rule_dept_and_sub_scope_empty_ids() {
let rule = DataScopeRule::new("d", "dept_id", "create_by");
let scope = DataScope::dept_and_sub(vec![]);
let sql = rule.apply_scope(&scope, 1001, 100);
assert_eq!(sql, "d.dept_id = 100");
}
#[test]
fn test_rule_self_only_scope() {
let rule = DataScopeRule::new("d", "dept_id", "create_by");
let scope = DataScope::self_only();
let sql = rule.apply_scope(&scope, 1001, 100);
assert_eq!(sql, "d.create_by = 1001");
}
#[test]
fn test_data_scope_context() {
let scope = DataScope::dept_and_sub(vec![100, 101]);
let ctx = DataScopeContext::new(scope, 1001, 100);
assert_eq!(ctx.user_id(), 1001);
assert_eq!(ctx.dept_id(), 100);
let rule = DataScopeRule::new("t", "dept_id", "user_id");
let sql = ctx.apply_rule(&rule);
assert_eq!(sql, "t.dept_id IN (100, 101)");
}
#[test]
fn test_with_data_scope() {
let scope = DataScope::self_only();
let ctx = DataScopeContext::new(scope, 42, 10);
let result = with_data_scope(ctx, || {
99
});
assert_eq!(result, 99);
}
#[tokio::test]
async fn test_with_data_scope_async() {
let scope = DataScope::department(200);
let ctx = DataScopeContext::new(scope, 42, 200);
let user_id = with_data_scope_async(ctx, || async {
tokio::task::yield_now().await;
let current = get_data_scope().await.expect("should have context");
current.user_id()
})
.await;
assert_eq!(user_id, 42);
}
#[tokio::test]
async fn test_get_data_scope_none_when_unset() {
assert!(get_data_scope().await.is_none());
}
#[tokio::test]
async fn test_apply_data_scope() {
let scope = DataScope::self_only();
let ctx = DataScopeContext::new(scope, 42, 10);
let result = with_data_scope_async(ctx, || async {
let rule = DataScopeRule::new("u", "dept_id", "owner_id");
apply_data_scope(&rule).await
})
.await;
assert_eq!(result, "u.owner_id = 42");
}
#[test]
fn test_build_and_parse_authority_all() {
let auth_str = build_data_scope_authority(&DataScopeType::All, 1, 10, &[]);
assert_eq!(auth_str, "DATASCOPE:ALL:1:10:");
let parsed = parse_data_scope_authority("ALL:1:10:").unwrap();
assert_eq!(parsed.0.scope_type, DataScopeType::All);
assert_eq!(parsed.1, 1);
assert_eq!(parsed.2, 10);
}
#[test]
fn test_build_and_parse_authority_dept_and_sub() {
let auth_str =
build_data_scope_authority(&DataScopeType::DeptAndSub, 1001, 100, &[100, 101, 102]);
assert_eq!(auth_str, "DATASCOPE:DEPT_AND_SUB:1001:100:100,101,102");
let parsed = parse_data_scope_authority("DEPT_AND_SUB:1001:100:100,101,102").unwrap();
assert_eq!(parsed.0.scope_type, DataScopeType::DeptAndSub);
assert_eq!(parsed.1, 1001);
assert_eq!(parsed.2, 100);
assert_eq!(parsed.0.dept_ids, vec![100, 101, 102]);
}
#[test]
fn test_parse_authority_self_only() {
let parsed = parse_data_scope_authority("SELF_ONLY:42:5:").unwrap();
assert_eq!(parsed.0.scope_type, DataScopeType::SelfOnly);
assert_eq!(parsed.1, 42);
assert_eq!(parsed.2, 5);
}
#[test]
fn test_parse_authority_invalid() {
assert!(parse_data_scope_authority("").is_none());
assert!(parse_data_scope_authority("INVALID:1:2:").is_none());
assert!(parse_data_scope_authority("ALL").is_none());
assert!(parse_data_scope_authority("ALL:abc:2:").is_none());
}
#[test]
fn test_default_scope_resolver() {
use crate::{Authentication, Authority};
let resolver = default_scope_resolver();
let auth = Authentication {
principal: "test".to_string(),
credentials: None,
authorities: vec![Authority::Role(crate::Role::Admin)],
authenticated: true,
details: None,
login_time: chrono::Utc::now(),
};
assert!(resolver(&auth).is_none());
let auth_str =
build_data_scope_authority(&DataScopeType::DeptAndSub, 1001, 100, &[100, 101]);
let auth = Authentication {
principal: "test".to_string(),
credentials: None,
authorities: vec![Authority::Permission(auth_str)],
authenticated: true,
details: None,
login_time: chrono::Utc::now(),
};
let result = resolver(&auth).unwrap();
assert_eq!(result.0.scope_type, DataScopeType::DeptAndSub);
assert_eq!(result.1, 1001);
assert_eq!(result.2, 100);
}
#[test]
fn test_rule_column_refs() {
let rule = DataScopeRule::new("dept", "dept_id", "create_by");
assert_eq!(rule.dept_ref(), "dept.dept_id");
assert_eq!(rule.user_ref(), "dept.create_by");
}
}