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}