Skip to main content

icebox/core/
governance.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::core::module::{Capability, Intent};
6use crate::core::safety::{DecisionRecord, PolicyDecision, PolicyRule};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum Role {
11    #[default]
12    Viewer,
13    Operator,
14    Admin,
15}
16
17impl Role {
18    pub fn as_str(&self) -> &'static str {
19        match self {
20            Role::Viewer => "viewer",
21            Role::Operator => "operator",
22            Role::Admin => "admin",
23        }
24    }
25}
26
27impl std::str::FromStr for Role {
28    type Err = String;
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        match s.to_lowercase().as_str() {
31            "viewer" => Ok(Role::Viewer),
32            "operator" => Ok(Role::Operator),
33            "admin" => Ok(Role::Admin),
34            other => Err(format!("unknown role: {other}")),
35        }
36    }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct PolicyPack {
41    pub name: String,
42    pub version: u64,
43    pub rules: Vec<PolicyRule>,
44}
45
46impl PolicyPack {
47    pub fn new(name: impl Into<String>, rules: Vec<PolicyRule>) -> Self {
48        PolicyPack {
49            name: name.into(),
50            version: 1,
51            rules,
52        }
53    }
54
55    pub fn set_rules(&mut self, rules: Vec<PolicyRule>) {
56        self.rules = rules;
57        self.version += 1;
58    }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum ApprovalStatus {
64    Pending,
65    Approved,
66    Denied,
67}
68
69impl ApprovalStatus {
70    pub fn as_str(&self) -> &'static str {
71        match self {
72            ApprovalStatus::Pending => "pending",
73            ApprovalStatus::Approved => "approved",
74            ApprovalStatus::Denied => "denied",
75        }
76    }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct ApprovalRequest {
81    pub id: u64,
82    pub module: String,
83    pub target: String,
84    pub reason: String,
85    #[serde(default)]
86    pub options: HashMap<String, String>,
87    pub status: ApprovalStatus,
88}
89
90#[derive(Debug)]
91pub struct ApprovalQueue {
92    items: Vec<ApprovalRequest>,
93    next_id: u64,
94}
95
96impl Default for ApprovalQueue {
97    fn default() -> Self {
98        ApprovalQueue {
99            items: Vec::new(),
100            next_id: 1,
101        }
102    }
103}
104
105impl ApprovalQueue {
106    pub fn request(
107        &mut self,
108        module: String,
109        target: String,
110        reason: String,
111        options: HashMap<String, String>,
112    ) -> u64 {
113        let id = self.next_id;
114        self.next_id += 1;
115        self.items.push(ApprovalRequest {
116            id,
117            module,
118            target,
119            reason,
120            options,
121            status: ApprovalStatus::Pending,
122        });
123        id
124    }
125
126    pub fn list(&self) -> Vec<ApprovalRequest> {
127        self.items.clone()
128    }
129
130    pub fn get(&self, id: u64) -> Option<&ApprovalRequest> {
131        self.items.iter().find(|i| i.id == id)
132    }
133
134    pub fn approve(&mut self, id: u64) -> bool {
135        match self.items.iter_mut().find(|i| i.id == id) {
136            Some(i) if i.status == ApprovalStatus::Pending => {
137                i.status = ApprovalStatus::Approved;
138                true
139            }
140            _ => false,
141        }
142    }
143
144    pub fn deny(&mut self, id: u64) -> bool {
145        match self.items.iter_mut().find(|i| i.id == id) {
146            Some(i) if i.status == ApprovalStatus::Pending => {
147                i.status = ApprovalStatus::Denied;
148                true
149            }
150            _ => false,
151        }
152    }
153}
154
155fn csv_field(s: &str) -> String {
156    if s.contains(',') || s.contains('"') || s.contains('\n') {
157        format!("\"{}\"", s.replace('"', "\"\""))
158    } else {
159        s.to_string()
160    }
161}
162
163pub fn audit_to_csv(records: &[DecisionRecord]) -> String {
164    let mut out =
165        String::from("at,target,module,capabilities,intents,impact,context,decision,reason\n");
166    for r in records {
167        let (decision, reason) = match &r.decision {
168            PolicyDecision::Allow => ("allow", String::new()),
169            PolicyDecision::RequireApproval(s) => ("require_approval", s.clone()),
170            PolicyDecision::Deny(s) => ("deny", s.clone()),
171        };
172        let caps: Vec<&str> = r.capabilities.iter().map(Capability::as_str).collect();
173        let intents: Vec<&str> = r.intents.iter().map(Intent::as_str).collect();
174        out.push_str(
175            &[
176                r.at.to_string(),
177                csv_field(&r.target),
178                csv_field(&r.module),
179                csv_field(&caps.join("|")),
180                csv_field(&intents.join("|")),
181                r.impact.as_str().to_string(),
182                format!("{:?}", r.context),
183                decision.to_string(),
184                csv_field(&reason),
185            ]
186            .join(","),
187        );
188        out.push('\n');
189    }
190    out
191}
192
193pub type PolicyPackStore = HashMap<String, PolicyPack>;
194
195pub fn role_allows(current: Role, required: Role) -> bool {
196    current >= required
197}