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 rules: Vec<RuleEntry>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct RuleEntry {
34 pub entity_type: EntityKind,
35 pub entity_id: String,
36 pub access: Access,
37 #[serde(default)]
38 pub roles: Vec<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub justification: Option<String>,
41}
42
43impl AccessControlConfig {
44 pub fn validate(&self) -> Result<(), AuthzError> {
45 let mut problems: Vec<String> = Vec::new();
46
47 for (idx, rule) in self.rules.iter().enumerate() {
48 if rule.entity_id.trim().is_empty() {
49 problems.push(format!("rules[{idx}]: entity_id is empty"));
50 }
51 if rule.roles.is_empty() {
52 problems.push(format!(
53 "rules[{idx}]: must declare at least one role — per-user rules belong to \
54 runtime state, not YAML, and attribute-based rules belong in an extension \
55 hook"
56 ));
57 }
58 for role in &rule.roles {
59 if role.trim().is_empty() {
60 problems.push(format!("rules[{idx}]: empty role string"));
61 }
62 }
63 }
64
65 if problems.is_empty() {
66 Ok(())
67 } else {
68 Err(AuthzError::Validation(problems.join("; ")))
69 }
70 }
71}