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 = if let Ok(content) = std::fs::read_to_string(path) {
match Self::parse_yaml_content(&content, path) {
Ok(cfg) => cfg,
Err(_e) => {
PermissionConfig::deny_all()
}
}
} else {
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"));
}
}