use super::types::{PermissionAction, PermissionConfig, RolePolicy};
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::Mutex as AsyncMutex;
#[derive(Debug, Error)]
pub enum PermissionProviderError {
#[error("Role '{0}' not found")]
RoleNotFound(String),
#[error("Failed to load config: {0}")]
LoadError(String),
#[error("Permission check failed: {0}")]
CheckError(String),
#[error("Unknown error: {0}")]
Unknown(String),
}
pub trait PermissionProvider: Send + Sync {
fn get_role_policy(&self, role: &str) -> Option<RolePolicy>;
fn check_access(
&self,
role: &str,
table: &str,
operation: PermissionAction,
) -> Result<bool, PermissionProviderError>;
fn get_roles(&self) -> Vec<String>;
fn has_role(&self, role: &str) -> bool {
self.get_role_policy(role).is_some()
}
}
pub trait RefreshablePermissionProvider: PermissionProvider {
fn refresh(&mut self) -> impl std::future::Future<Output = Result<(), PermissionProviderError>> + Send;
}
#[derive(Debug, Clone)]
pub struct YamlPermissionProvider {
config: Arc<PermissionConfig>,
path: Option<String>,
}
impl YamlPermissionProvider {
pub fn new(path: &str) -> Self {
let config = match std::fs::read_to_string(path) {
Ok(content) => match Self::parse_yaml_content(&content, path) {
Ok(cfg) => cfg,
Err(e) => {
#[cfg(feature = "tracing")]
tracing::warn!(
path = path,
error = %e,
"YamlPermissionProvider: YAML 解析失败,回退到 deny_all 策略"
);
#[cfg(not(feature = "tracing"))]
eprintln!(
"[WARN] YamlPermissionProvider: YAML 解析失败 (path='{}'): {},回退到 deny_all 策略",
path, e
);
PermissionConfig::deny_all()
}
},
Err(e) => {
#[cfg(feature = "tracing")]
tracing::warn!(
path = path,
error = %e,
"YamlPermissionProvider: 权限配置文件读取失败,回退到 deny_all 策略"
);
#[cfg(not(feature = "tracing"))]
eprintln!(
"[WARN] YamlPermissionProvider: 权限配置文件读取失败 (path='{}'): {},回退到 deny_all 策略",
path, e
);
PermissionConfig::deny_all()
}
};
Self {
config: Arc::new(config),
path: Some(path.to_string()),
}
}
#[cfg(feature = "json")]
fn parse_json_content(content: &str, source: &str) -> Result<PermissionConfig, String> {
serde_json::from_str(content).map_err(|e| format!("JSON parse error in '{}': {}", source, e))
}
fn parse_yaml_content(content: &str, source: &str) -> Result<PermissionConfig, String> {
#[cfg(feature = "json")]
{
Self::parse_json_content(content, source)
}
#[cfg(not(feature = "json"))]
{
#[cfg(feature = "yaml")]
{
serde_yaml_ng::from_str(content).map_err(|e| format!("YAML parse error in '{}': {}", source, e))
}
#[cfg(not(feature = "yaml"))]
{
Err(format!(
"Cannot parse permission config from '{}': neither JSON nor YAML support available",
source
))
}
}
}
pub fn from_config(config: PermissionConfig) -> Self {
Self {
config: Arc::new(config),
path: None,
}
}
}
impl PermissionProvider for YamlPermissionProvider {
fn get_role_policy(&self, role: &str) -> Option<RolePolicy> {
self.config.get_role_policy(role).cloned()
}
fn check_access(
&self,
role: &str,
table: &str,
operation: PermissionAction,
) -> Result<bool, PermissionProviderError> {
Ok(self.config.check_access(role, table, operation))
}
fn get_roles(&self) -> Vec<String> {
self.config.roles.keys().cloned().collect()
}
}
impl RefreshablePermissionProvider for YamlPermissionProvider {
async fn refresh(&mut self) -> Result<(), PermissionProviderError> {
if let Some(ref path) = self.path {
match tokio::fs::read_to_string(path).await {
Ok(content) => match parse_permission_yaml_async(&content, path).await {
Ok(config) => {
self.config = Arc::new(config);
Ok(())
}
Err(e) => Err(PermissionProviderError::LoadError(e.to_string())),
},
Err(e) => Err(PermissionProviderError::LoadError(e.to_string())),
}
} else {
Ok(())
}
}
}
async fn parse_permission_yaml_async(content: &str, source: &str) -> Result<PermissionConfig, String> {
#[cfg(feature = "json")]
{
serde_json::from_str(content).map_err(|e| format!("Config deserialization error in '{}': {}", source, e))
}
#[cfg(not(feature = "json"))]
{
#[cfg(feature = "yaml")]
{
serde_yaml_ng::from_str(content).map_err(|e| format!("YAML parse error in '{}': {}", source, e))
}
#[cfg(not(feature = "yaml"))]
{
Err(format!(
"Cannot parse permission config from '{}': neither JSON nor YAML support available",
source
))
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryPermissionProvider {
config: Arc<AsyncMutex<PermissionConfig>>,
}
impl Default for MemoryPermissionProvider {
fn default() -> Self {
Self {
config: Arc::new(AsyncMutex::new(PermissionConfig::default())),
}
}
}
impl MemoryPermissionProvider {
pub fn new() -> Self {
Self::default()
}
pub async fn add_role(&self, role: &str, policy: RolePolicy) {
let mut config = self.config.lock().await;
config.roles.insert(role.to_string(), policy);
}
pub async fn remove_role(&self, role: &str) -> bool {
let mut config = self.config.lock().await;
config.roles.remove(role).is_some()
}
}
impl PermissionProvider for MemoryPermissionProvider {
fn get_role_policy(&self, role: &str) -> Option<RolePolicy> {
if let Ok(config) = self.config.try_lock() {
config.get_role_policy(role).cloned()
} else {
None
}
}
fn check_access(
&self,
role: &str,
table: &str,
operation: PermissionAction,
) -> Result<bool, PermissionProviderError> {
if let Ok(config) = self.config.try_lock() {
Ok(config.check_access(role, table, operation))
} else {
Ok(false)
}
}
fn get_roles(&self) -> Vec<String> {
if let Ok(config) = self.config.try_lock() {
config.roles.keys().cloned().collect()
} else {
Vec::new()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::access::permission::types::TablePermission;
#[tokio::test]
async fn memory_provider_new_is_empty() {
let p = MemoryPermissionProvider::new();
assert!(p.get_roles().is_empty());
}
#[tokio::test]
async fn memory_provider_add_role() {
let p = MemoryPermissionProvider::new();
p.add_role(
"admin",
RolePolicy {
tables: vec![TablePermission {
name: "*".into(),
operations: vec![PermissionAction::Select, PermissionAction::Insert],
}],
},
)
.await;
let policy = p.get_role_policy("admin");
assert!(policy.is_some());
}
#[tokio::test]
async fn memory_provider_check_access() {
let p = MemoryPermissionProvider::new();
p.add_role(
"reader",
RolePolicy {
tables: vec![TablePermission {
name: "docs".into(),
operations: vec![PermissionAction::Select],
}],
},
)
.await;
assert!(p.check_access("reader", "docs", PermissionAction::Select).unwrap());
assert!(!p.check_access("reader", "docs", PermissionAction::Delete).unwrap());
assert!(!p.check_access("ghost", "docs", PermissionAction::Select).unwrap());
}
#[tokio::test]
async fn memory_provider_remove_role() {
let p = MemoryPermissionProvider::new();
p.add_role("temp", RolePolicy::default()).await;
assert!(p.remove_role("temp").await);
assert!(!p.remove_role("temp").await);
}
#[tokio::test]
async fn memory_provider_get_roles() {
let p = MemoryPermissionProvider::new();
p.add_role("a", RolePolicy::default()).await;
p.add_role("b", RolePolicy::default()).await;
let mut roles = p.get_roles();
roles.sort();
assert_eq!(roles, vec!["a", "b"]);
}
#[test]
fn yaml_provider_from_config() {
let mut config = PermissionConfig::default();
config.roles.insert(
"admin".into(),
RolePolicy {
tables: vec![TablePermission {
name: "*".into(),
operations: vec![PermissionAction::Select],
}],
},
);
let p = YamlPermissionProvider::from_config(config);
assert!(p.get_role_policy("admin").is_some());
assert!(p.get_role_policy("nobody").is_none());
}
#[test]
fn yaml_provider_check_access_from_config() {
let mut config = PermissionConfig::default();
config.roles.insert(
"viewer".into(),
RolePolicy {
tables: vec![TablePermission {
name: "reports".into(),
operations: vec![PermissionAction::Select],
}],
},
);
let p = YamlPermissionProvider::from_config(config);
assert!(p.check_access("viewer", "reports", PermissionAction::Select).unwrap());
assert!(!p.check_access("viewer", "reports", PermissionAction::Update).unwrap());
assert!(!p.check_access("viewer", "secret", PermissionAction::Select).unwrap());
}
#[test]
fn permission_provider_has_role() {
let mut config = PermissionConfig::default();
config.roles.insert("admin".into(), RolePolicy::default());
let p: Arc<dyn PermissionProvider> = Arc::new(YamlPermissionProvider::from_config(config));
assert!(p.has_role("admin"));
assert!(!p.has_role("nobody"));
}
#[test]
fn yaml_provider_new_nonexistent_file_falls_back_to_deny_all() {
let p = YamlPermissionProvider::new("/nonexistent/path/permissions.yaml");
assert!(
p.get_roles().is_empty(),
"nonexistent file should fall back to deny_all with empty roles"
);
assert!(p.get_role_policy("admin").is_none());
let result = p.check_access("admin", "any_table", PermissionAction::Select);
assert!(result.is_ok());
assert!(!result.unwrap(), "deny_all should reject all access");
}
#[test]
fn yaml_provider_new_with_valid_file_loads_roles() {
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("dbnexus_test_permissions_valid.yaml");
let yaml_content = r#"{"roles": {"admin": {"tables": [{"name": "*", "operations": ["select"]}]}}}"#;
std::fs::write(&file_path, yaml_content).expect("failed to write temp file");
let p = YamlPermissionProvider::new(file_path.to_str().unwrap());
let roles = p.get_roles();
assert!(
roles.iter().any(|r| r == "admin"),
"admin role should be loaded from valid file, got: {:?}",
roles
);
let policy = p.get_role_policy("admin");
assert!(policy.is_some(), "admin policy should be Some");
assert!(
p.check_access("admin", "any_table", PermissionAction::Select).unwrap(),
"admin should have Select on any_table"
);
let _ = std::fs::remove_file(&file_path);
}
#[test]
fn yaml_provider_new_with_malformed_file_falls_back_to_deny_all() {
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("dbnexus_test_permissions_malformed.yaml");
let malformed_content = "this is not valid: yaml: content: [[[";
std::fs::write(&file_path, malformed_content).expect("failed to write temp file");
let p = YamlPermissionProvider::new(file_path.to_str().unwrap());
assert!(
p.get_roles().is_empty(),
"malformed file should fall back to deny_all with empty roles"
);
let _ = std::fs::remove_file(&file_path);
}
#[tokio::test]
async fn yaml_provider_refresh_no_path_returns_ok() {
let mut config = PermissionConfig::default();
config.roles.insert("admin".into(), RolePolicy::default());
let mut p = YamlPermissionProvider::from_config(config);
let result = p.refresh().await;
assert!(result.is_ok(), "refresh on pathless provider should return Ok");
}
#[tokio::test]
async fn yaml_provider_refresh_valid_file_succeeds() {
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("dbnexus_test_refresh_valid.yaml");
let yaml_content = r#"{"roles": {"admin": {"tables": [{"name": "*", "operations": ["select"]}]}}}"#;
std::fs::write(&file_path, yaml_content).expect("failed to write temp file");
let mut p = YamlPermissionProvider::new(file_path.to_str().unwrap());
assert!(p.get_role_policy("admin").is_some());
let updated_content = r#"{"roles": {"admin": {"tables": [{"name": "*", "operations": ["select"]}]}, "user": {"tables": [{"name": "docs", "operations": ["select"]}]}}}"#;
std::fs::write(&file_path, updated_content).expect("failed to rewrite temp file");
let result = p.refresh().await;
assert!(result.is_ok(), "refresh should succeed with valid file");
let roles = p.get_roles();
assert!(
roles.iter().any(|r| r == "user"),
"refresh should load new 'user' role: {:?}",
roles
);
let _ = std::fs::remove_file(&file_path);
}
#[tokio::test]
async fn yaml_provider_refresh_file_deleted_returns_err() {
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("dbnexus_test_refresh_deleted.yaml");
let yaml_content = r#"{"roles": {"admin": {"tables": [{"name": "*", "operations": ["select"]}]}}}"#;
std::fs::write(&file_path, yaml_content).expect("failed to write temp file");
let mut p = YamlPermissionProvider::new(file_path.to_str().unwrap());
std::fs::remove_file(&file_path).expect("failed to delete temp file");
let result = p.refresh().await;
assert!(result.is_err(), "refresh should fail when file is deleted");
match result {
Err(PermissionProviderError::LoadError(_)) => { }
Err(other) => panic!("expected LoadError, got {:?}", other),
Ok(_) => panic!("expected error, got Ok"),
}
}
#[tokio::test]
async fn yaml_provider_refresh_malformed_returns_err() {
let temp_dir = std::env::temp_dir();
let file_path = temp_dir.join("dbnexus_test_refresh_malformed.yaml");
let yaml_content = r#"{"roles": {"admin": {"tables": [{"name": "*", "operations": ["select"]}]}}}"#;
std::fs::write(&file_path, yaml_content).expect("failed to write temp file");
let mut p = YamlPermissionProvider::new(file_path.to_str().unwrap());
let malformed = "not valid: yaml: [[[";
std::fs::write(&file_path, malformed).expect("failed to rewrite temp file");
let result = p.refresh().await;
assert!(result.is_err(), "refresh should fail with malformed file");
match result {
Err(PermissionProviderError::LoadError(_)) => { }
Err(other) => panic!("expected LoadError, got {:?}", other),
Ok(_) => panic!("expected error, got Ok"),
}
let _ = std::fs::remove_file(&file_path);
}
#[tokio::test]
async fn yaml_provider_is_refreshable() {
let mut config = PermissionConfig::default();
config.roles.insert("admin".into(), RolePolicy::default());
let mut p = YamlPermissionProvider::from_config(config);
use super::RefreshablePermissionProvider;
let result = <YamlPermissionProvider as RefreshablePermissionProvider>::refresh(&mut p).await;
assert!(result.is_ok());
}
}