use serde::{Deserialize, Serialize};
use super::error::AuthzError;
use super::types::{Access, EntityKind};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AccessControlConfig {
#[serde(default)]
pub departments: Vec<DepartmentEntry>,
#[serde(default)]
pub rules: Vec<RuleEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DepartmentEntry {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub manager_email: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RuleEntry {
pub entity_type: EntityKind,
pub entity_id: String,
pub access: Access,
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub departments: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub justification: Option<String>,
}
impl AccessControlConfig {
pub fn validate(&self) -> Result<(), AuthzError> {
let mut problems: Vec<String> = Vec::new();
let mut declared: std::collections::HashSet<&str> =
std::collections::HashSet::with_capacity(self.departments.len());
for (idx, dept) in self.departments.iter().enumerate() {
if dept.name.trim().is_empty() {
problems.push(format!("departments[{idx}]: name is empty"));
continue;
}
if !declared.insert(dept.name.as_str()) {
problems.push(format!(
"departments[{idx}]: duplicate department name '{}'",
dept.name
));
}
}
for (idx, rule) in self.rules.iter().enumerate() {
if rule.entity_id.trim().is_empty() {
problems.push(format!("rules[{idx}]: entity_id is empty"));
}
if rule.roles.is_empty() && rule.departments.is_empty() {
problems.push(format!(
"rules[{idx}]: must declare at least one of roles[] or departments[] — \
per-user rules belong to runtime state, not YAML"
));
}
for dept in &rule.departments {
if !declared.contains(dept.as_str()) {
problems.push(format!(
"rules[{idx}]: references undeclared department '{dept}' (add it to the \
top-level departments: list)"
));
}
}
for role in &rule.roles {
if role.trim().is_empty() {
problems.push(format!("rules[{idx}]: empty role string"));
}
}
}
if problems.is_empty() {
Ok(())
} else {
Err(AuthzError::Validation(problems.join("; ")))
}
}
}