systemprompt_security/authz/
config.rs1use serde::{Deserialize, Serialize};
20
21use super::error::AuthzError;
22use super::types::{Access, EntityKind};
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct AccessControlConfig {
27 #[serde(default)]
28 pub departments: Vec<DepartmentEntry>,
29 #[serde(default)]
30 pub rules: Vec<RuleEntry>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct DepartmentEntry {
36 pub name: String,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub description: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub manager_email: Option<String>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct RuleEntry {
46 pub entity_type: EntityKind,
47 pub entity_id: String,
48 pub access: Access,
49 #[serde(default)]
50 pub roles: Vec<String>,
51 #[serde(default)]
52 pub departments: Vec<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub justification: Option<String>,
55}
56
57impl AccessControlConfig {
58 pub fn validate(&self) -> Result<(), AuthzError> {
59 let mut problems: Vec<String> = Vec::new();
60
61 let mut declared: std::collections::HashSet<&str> =
62 std::collections::HashSet::with_capacity(self.departments.len());
63 for (idx, dept) in self.departments.iter().enumerate() {
64 if dept.name.trim().is_empty() {
65 problems.push(format!("departments[{idx}]: name is empty"));
66 continue;
67 }
68 if !declared.insert(dept.name.as_str()) {
69 problems.push(format!(
70 "departments[{idx}]: duplicate department name '{}'",
71 dept.name
72 ));
73 }
74 }
75
76 for (idx, rule) in self.rules.iter().enumerate() {
77 if rule.entity_id.trim().is_empty() {
78 problems.push(format!("rules[{idx}]: entity_id is empty"));
79 }
80 if rule.roles.is_empty() && rule.departments.is_empty() {
81 problems.push(format!(
82 "rules[{idx}]: must declare at least one of roles[] or departments[] — \
83 per-user rules belong to runtime state, not YAML"
84 ));
85 }
86 for dept in &rule.departments {
87 if !declared.contains(dept.as_str()) {
88 problems.push(format!(
89 "rules[{idx}]: references undeclared department '{dept}' (add it to the \
90 top-level departments: list)"
91 ));
92 }
93 }
94 for role in &rule.roles {
95 if role.trim().is_empty() {
96 problems.push(format!("rules[{idx}]: empty role string"));
97 }
98 }
99 }
100
101 if problems.is_empty() {
102 Ok(())
103 } else {
104 Err(AuthzError::Validation(problems.join("; ")))
105 }
106 }
107}