use super::error::{PermissionConfigError, PermissionError};
use serde::Deserialize;
#[derive(Debug, Clone, Copy, Default, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DefaultPolicy {
#[default]
DenyAll,
AllowAll,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PermissionConfig {
#[serde(default)]
pub policy_path: Option<String>,
#[serde(default)]
pub default_policy: DefaultPolicy,
#[serde(default = "PermissionConfig::default_admin_role")]
pub admin_role: String,
#[serde(default)]
pub rate_limit_enabled: bool,
#[serde(default = "PermissionConfig::default_rate_limit_max")]
pub rate_limit_max_requests: u32,
}
impl Default for PermissionConfig {
fn default() -> Self {
Self {
policy_path: None,
default_policy: DefaultPolicy::default(),
admin_role: Self::default_admin_role(),
rate_limit_enabled: false,
rate_limit_max_requests: Self::default_rate_limit_max(),
}
}
}
impl PermissionConfig {
pub fn validate(&self) -> Result<(), PermissionConfigError> {
if self.admin_role.is_empty() {
return Err(PermissionConfigError::MissingField("admin_role".into()));
}
if self.rate_limit_enabled && self.rate_limit_max_requests == 0 {
return Err(PermissionConfigError::InvalidValue {
field: "rate_limit_max_requests".into(),
reason: "must be greater than 0 when rate limiting enabled".into(),
});
}
if let Some(path) = &self.policy_path {
Self::validate_policy_path(path).map_err(|e| PermissionConfigError::InvalidValue {
field: "policy_path".into(),
reason: e.to_string(),
})?;
}
Ok(())
}
pub(crate) fn validate_policy_path(path: &str) -> Result<(), PermissionError> {
let path = std::path::Path::new(path);
if !path.is_absolute() {
return Err(PermissionError::InvalidPolicy(format!(
"policy path must be absolute: {}",
path.display()
)));
}
if path
.components()
.any(|c| c == std::path::Component::ParentDir)
{
return Err(PermissionError::InvalidPolicy(format!(
"policy path must not contain '..' components: {}",
path.display()
)));
}
Ok(())
}
fn default_admin_role() -> String {
"admin".into()
}
fn default_rate_limit_max() -> u32 {
100
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_ok_without_policy_path() {
assert!(PermissionConfig::default().validate().is_ok());
}
#[test]
fn validate_rejects_relative_policy_path() {
assert!(PermissionConfig::validate_policy_path("policies/roles.yaml").is_err());
assert!(PermissionConfig::validate_policy_path("").is_err());
}
#[test]
fn validate_rejects_parent_dir_component() {
assert!(PermissionConfig::validate_policy_path("/etc/dbnexus/../secrets.yaml").is_err());
assert!(PermissionConfig::validate_policy_path("..").is_err());
}
#[test]
fn validate_accepts_absolute_path_without_parent_dir() {
assert!(PermissionConfig::validate_policy_path("/etc/dbnexus/policies.yaml").is_ok());
assert!(PermissionConfig::validate_policy_path("/etc/dbnexus/policies..yaml").is_ok());
}
#[test]
fn validate_checks_policy_path_when_present() {
let cfg = PermissionConfig {
policy_path: Some("relative/roles.yaml".into()),
..PermissionConfig::default()
};
let err = cfg.validate().unwrap_err();
assert!(
matches!(err, PermissionConfigError::InvalidValue { ref field, .. } if field == "policy_path"),
"expected InvalidValue for policy_path, got {err:?}"
);
}
}