1use crate::pair_decision::{InboundMode, OrgPolicy};
19use anyhow::Result;
20use serde_json::json;
21use std::collections::HashMap;
22use std::path::Path;
23
24const FILE: &str = "org_policies.json";
25
26#[derive(Debug, Clone, Default)]
29pub struct FileOrgPolicy {
30 orgs: HashMap<String, InboundMode>,
31}
32
33impl FileOrgPolicy {
34 pub fn load() -> Self {
37 match crate::config::config_dir() {
38 Ok(dir) => Self::load_path(&dir.join(FILE)),
39 Err(_) => Self::default(),
40 }
41 }
42
43 pub fn load_path(path: &Path) -> Self {
45 let Ok(bytes) = std::fs::read(path) else {
46 return Self::default();
47 };
48 let Ok(json) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
49 return Self::default();
50 };
51 let mut orgs = HashMap::new();
52 if let Some(map) = json.get("orgs").and_then(|v| v.as_object()) {
53 for (org_did, entry) in map {
54 if let Some(mode) = entry
55 .get("inbound")
56 .and_then(|v| v.as_str())
57 .and_then(parse_mode)
58 {
59 orgs.insert(org_did.clone(), mode);
60 }
61 }
62 }
63 Self { orgs }
64 }
65
66 pub fn set(&mut self, org_did: &str, mode: InboundMode) {
68 self.orgs.insert(org_did.to_string(), mode);
69 }
70
71 pub fn remove(&mut self, org_did: &str) {
73 self.orgs.remove(org_did);
74 }
75
76 pub fn len(&self) -> usize {
78 self.orgs.len()
79 }
80
81 pub fn is_empty(&self) -> bool {
82 self.orgs.is_empty()
83 }
84
85 pub fn save(&self) -> Result<()> {
87 let dir = crate::config::config_dir()?;
88 std::fs::create_dir_all(&dir)?;
89 self.save_path(&dir.join(FILE))?;
90 Ok(())
91 }
92
93 pub fn save_path(&self, path: &Path) -> std::io::Result<()> {
95 std::fs::write(path, self.to_json())
96 }
97
98 fn to_json(&self) -> String {
99 let orgs: serde_json::Map<String, serde_json::Value> = self
100 .orgs
101 .iter()
102 .map(|(k, v)| (k.clone(), json!({ "inbound": mode_str(*v) })))
103 .collect();
104 serde_json::to_string_pretty(&json!({ "version": 1, "orgs": orgs }))
105 .unwrap_or_else(|_| "{}".into())
106 }
107}
108
109impl OrgPolicy for FileOrgPolicy {
110 fn inbound_mode(&self, org_did: &str) -> Option<InboundMode> {
111 self.orgs.get(org_did).copied()
112 }
113}
114
115fn parse_mode(s: &str) -> Option<InboundMode> {
116 match s {
117 "auto" => Some(InboundMode::Auto),
118 "notify" => Some(InboundMode::Notify),
119 _ => None,
120 }
121}
122
123fn mode_str(m: InboundMode) -> &'static str {
124 match m {
125 InboundMode::Auto => "auto",
126 InboundMode::Notify => "notify",
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn tmp(name: &str) -> std::path::PathBuf {
135 std::env::temp_dir().join(format!("wire-orgpol-{}-{name}.json", std::process::id()))
136 }
137
138 #[test]
139 fn missing_file_is_default_deny() {
140 let p = tmp("missing");
141 let _ = std::fs::remove_file(&p);
142 let pol = FileOrgPolicy::load_path(&p);
143 assert!(pol.is_empty());
144 assert_eq!(pol.inbound_mode("did:wire:org:slanchaai-1"), None);
145 }
146
147 #[test]
148 fn malformed_file_is_default_deny() {
149 let p = tmp("malformed");
150 std::fs::write(&p, b"not json {{{").unwrap();
151 let pol = FileOrgPolicy::load_path(&p);
152 assert!(pol.is_empty(), "malformed policy must fail closed to empty");
153 let _ = std::fs::remove_file(&p);
154 }
155
156 #[test]
157 fn set_save_load_roundtrip() {
158 let p = tmp("roundtrip");
159 let mut pol = FileOrgPolicy::default();
160 pol.set("did:wire:org:slanchaai-1", InboundMode::Auto);
161 pol.set("did:wire:org:contractor-2", InboundMode::Notify);
162 pol.save_path(&p).unwrap();
163
164 let loaded = FileOrgPolicy::load_path(&p);
165 assert_eq!(
166 loaded.inbound_mode("did:wire:org:slanchaai-1"),
167 Some(InboundMode::Auto)
168 );
169 assert_eq!(
170 loaded.inbound_mode("did:wire:org:contractor-2"),
171 Some(InboundMode::Notify)
172 );
173 assert_eq!(loaded.inbound_mode("did:wire:org:unknown-9"), None);
174 let _ = std::fs::remove_file(&p);
175 }
176
177 #[test]
178 fn unknown_mode_string_is_skipped() {
179 let p = tmp("badmode");
180 std::fs::write(
181 &p,
182 br#"{"version":1,"orgs":{"did:wire:org:x-1":{"inbound":"superuser"}}}"#,
183 )
184 .unwrap();
185 let pol = FileOrgPolicy::load_path(&p);
186 assert_eq!(pol.inbound_mode("did:wire:org:x-1"), None);
187 let _ = std::fs::remove_file(&p);
188 }
189
190 #[test]
191 fn remove_drops_org() {
192 let mut pol = FileOrgPolicy::default();
193 pol.set("did:wire:org:x-1", InboundMode::Auto);
194 pol.remove("did:wire:org:x-1");
195 assert_eq!(pol.inbound_mode("did:wire:org:x-1"), None);
196 }
197}