use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PermissionError {
#[error("Cache capacity must be non-zero")]
InvalidCacheCapacity,
#[error("Role '{0}' not found in permission config")]
RoleNotFound(String),
#[error("Failed to load permission config: {0}")]
ConfigLoadError(String),
#[error("Permission check rate limited")]
RateLimited,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionAction {
Select,
Insert,
Update,
Delete,
}
impl std::fmt::Display for PermissionAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PermissionAction::Select => write!(f, "SELECT"),
PermissionAction::Insert => write!(f, "INSERT"),
PermissionAction::Update => write!(f, "UPDATE"),
PermissionAction::Delete => write!(f, "DELETE"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TablePermission {
pub name: String,
pub operations: Vec<PermissionAction>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RolePolicy {
pub tables: Vec<TablePermission>,
}
impl RolePolicy {
pub fn allows(&self, table: &str, operation: &PermissionAction) -> bool {
for perm in &self.tables {
if perm.name == "*" || perm.name == table {
if perm.operations.contains(operation) {
return true;
}
}
}
false
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PermissionConfig {
#[serde(default)]
pub roles: HashMap<String, RolePolicy>,
}
impl PermissionConfig {
#[cfg(feature = "yaml")]
pub fn from_yaml_str(yaml: &str) -> Result<Self, serde_yaml_ng::Error> {
serde_yaml_ng::from_str(yaml)
}
pub fn from_json_str(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn get_role_policy(&self, role: &str) -> Option<&RolePolicy> {
self.roles.get(role)
}
pub fn check_access(&self, role: &str, table: &str, operation: PermissionAction) -> bool {
if let Some(policy) = self.get_role_policy(role) {
policy.allows(table, &operation)
} else {
false
}
}
pub fn deny_all() -> Self {
Self {
roles: HashMap::new(), }
}
pub fn allow_all() -> Self {
Self {
roles: HashMap::from([(
"admin".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "*".to_string(),
operations: vec![
PermissionAction::Select,
PermissionAction::Insert,
PermissionAction::Update,
PermissionAction::Delete,
],
}],
},
)]),
}
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
if self.roles.is_empty() {
errors.push("No roles defined in permission config".to_string());
}
for (role_name, policy) in &self.roles {
if policy.tables.is_empty() {
errors.push(format!("Role '{}' has no table permissions defined", role_name));
}
for table_perm in &policy.tables {
if table_perm.name.trim().is_empty() {
errors.push(format!("Role '{}' has a table permission with empty name", role_name));
}
if table_perm.operations.is_empty() {
errors.push(format!(
"Table '{}' in role '{}' has no operations defined",
table_perm.name, role_name
));
}
}
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
pub fn validate_with_first_error(&self) -> Result<(), String> {
self.validate().map_err(|errors| errors.join("; "))
}
pub fn is_ddl_allowed_role(&self, role: &str) -> bool {
if let Some(policy) = self.get_role_policy(role) {
if let Some(table_perm) = policy.tables.iter().find(|tp| tp.name == "*") {
table_perm.operations.contains(&PermissionAction::Select)
&& table_perm.operations.contains(&PermissionAction::Insert)
&& table_perm.operations.contains(&PermissionAction::Update)
&& table_perm.operations.contains(&PermissionAction::Delete)
} else {
false
}
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_operation_display() {
assert_eq!(PermissionAction::Select.to_string(), "SELECT");
assert_eq!(PermissionAction::Insert.to_string(), "INSERT");
assert_eq!(PermissionAction::Update.to_string(), "UPDATE");
assert_eq!(PermissionAction::Delete.to_string(), "DELETE");
}
#[test]
fn test_role_policy_allows() {
let policy = RolePolicy {
tables: vec![
TablePermission {
name: "users".to_string(),
operations: vec![PermissionAction::Select, PermissionAction::Insert],
},
TablePermission {
name: "*".to_string(),
operations: vec![PermissionAction::Select],
},
],
};
assert!(policy.allows("users", &PermissionAction::Select));
assert!(policy.allows("users", &PermissionAction::Insert));
assert!(!policy.allows("users", &PermissionAction::Delete));
assert!(policy.allows("orders", &PermissionAction::Select));
assert!(!policy.allows("orders", &PermissionAction::Update));
}
#[cfg(feature = "yaml")]
#[test]
fn test_permission_config_yaml_parsing() {
let yaml = r#"
{
"roles": {
"admin": {
"tables": [
{
"name": "users",
"operations": ["select", "insert", "update", "delete"]
}
]
},
"user": {
"tables": [
{
"name": "users",
"operations": ["select"]
}
]
}
}
}
"#;
let config: PermissionConfig = serde_yaml_ng::from_str(yaml).expect("Failed to parse YAML");
let admin_policy = config.get_role_policy("admin").unwrap();
assert!(admin_policy.allows("users", &PermissionAction::Select));
assert!(admin_policy.allows("users", &PermissionAction::Delete));
let user_policy = config.get_role_policy("user").unwrap();
assert!(user_policy.allows("users", &PermissionAction::Select));
assert!(!user_policy.allows("users", &PermissionAction::Insert));
assert!(config.get_role_policy("guest").is_none());
}
#[test]
fn test_permission_config_yaml_parsing_struct() {
let config = PermissionConfig {
roles: {
let mut map = HashMap::new();
map.insert(
"admin".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "users".to_string(),
operations: vec![
PermissionAction::Select,
PermissionAction::Insert,
PermissionAction::Update,
PermissionAction::Delete,
],
}],
},
);
map.insert(
"user".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "users".to_string(),
operations: vec![PermissionAction::Select],
}],
},
);
map
},
};
let admin_policy = config.get_role_policy("admin").unwrap();
assert!(admin_policy.allows("users", &PermissionAction::Select));
assert!(admin_policy.allows("users", &PermissionAction::Delete));
let user_policy = config.get_role_policy("user").unwrap();
assert!(user_policy.allows("users", &PermissionAction::Select));
assert!(!user_policy.allows("users", &PermissionAction::Insert));
assert!(config.get_role_policy("guest").is_none());
}
#[test]
fn test_permission_config_check_access() {
let config = PermissionConfig {
roles: {
let mut map = HashMap::new();
map.insert(
"admin".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "*".to_string(),
operations: vec![PermissionAction::Select, PermissionAction::Insert],
}],
},
);
map
},
};
assert!(config.check_access("admin", "users", PermissionAction::Select));
assert!(!config.check_access("admin", "users", PermissionAction::Delete));
assert!(!config.check_access("guest", "users", PermissionAction::Select));
}
#[test]
fn test_permission_config_validation_valid() {
let config = PermissionConfig {
roles: {
let mut map = HashMap::new();
map.insert(
"admin".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "users".to_string(),
operations: vec![PermissionAction::Select, PermissionAction::Insert],
}],
},
);
map
},
};
assert!(config.validate().is_ok());
assert!(config.validate_with_first_error().is_ok());
}
#[test]
fn test_permission_config_validation_empty_roles() {
let config = PermissionConfig { roles: HashMap::new() };
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("No roles defined")));
}
#[test]
fn test_permission_config_validation_empty_table_permissions() {
let config = PermissionConfig {
roles: {
let mut map = HashMap::new();
map.insert(
"admin".to_string(),
RolePolicy {
tables: vec![], },
);
map
},
};
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("has no table permissions")));
}
#[test]
fn test_permission_config_validation_empty_operations() {
let config = PermissionConfig {
roles: {
let mut map = HashMap::new();
map.insert(
"admin".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "users".to_string(),
operations: vec![], }],
},
);
map
},
};
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("has no operations defined")));
}
}