interlink/
policy_store.rs1use std::fs::OpenOptions;
4use std::path::{Path, PathBuf};
5
6use anyhow::Result;
7use fs2::FileExt;
8
9use crate::identity::AgentId;
10use crate::policy::Policy;
11
12pub struct PolicyStore {
13 path: PathBuf,
14}
15
16impl PolicyStore {
17 pub fn open(path: &Path) -> Result<Self> {
18 let path = path.canonicalize()?;
20 Policy::load(&path)?;
21 Ok(Self { path })
22 }
23
24 pub fn read(&self) -> Result<Policy> {
25 Policy::load(&self.path)
26 }
27
28 fn update<T>(&self, change: impl FnOnce(&mut Policy) -> Result<T>) -> Result<T> {
29 let lock = OpenOptions::new()
31 .create(true)
32 .truncate(false)
33 .write(true)
34 .open(self.path.with_extension("json.lock"))?;
35 lock.lock_exclusive()?;
36 let mut policy = self.read()?;
37 let result = change(&mut policy)?;
38 policy.save(&self.path)?;
39 Ok(result)
40 }
41
42 pub fn add(&self, name: &str, key: &str) -> Result<()> {
43 self.update(|policy| policy.add(name, key))
44 }
45
46 pub fn add_for_pairing(&self, name: &str, key: &str) -> Result<String> {
47 self.update(|policy| {
48 if let Some(peer) = policy.peer(AgentId::from_b64(key)?) {
51 return Ok(peer.petname.clone());
52 }
53 policy.add(name, key)?;
54 Ok(name.to_string())
55 })
56 }
57
58 pub fn remove(&self, name: &str) -> Result<bool> {
59 self.update(|policy| Ok(policy.remove(name)))
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use crate::identity::AgentKey;
67 use std::thread;
68
69 #[test]
70 fn independent_sessions_merge_updates_and_observe_removals() {
71 let dir = tempfile::tempdir().unwrap();
72 let path = dir.path().join("peers.json");
73 std::fs::write(&path, "{}").unwrap();
74 let first = PolicyStore::open(&path).unwrap();
75 let second = PolicyStore::open(&path).unwrap();
76 let alice = AgentKey::generate().unwrap().id().to_b64();
77 let bob = AgentKey::generate().unwrap().id().to_b64();
78 thread::scope(|scope| {
79 scope.spawn(|| first.add("alice", &alice).unwrap());
80 scope.spawn(|| second.add("bob", &bob).unwrap());
81 });
82 assert_eq!(first.read().unwrap().len(), 2);
83 first.remove("alice").unwrap();
84 assert!(second.read().unwrap().resolve("alice").is_err());
85 second.add("bob", &bob).unwrap();
86 assert_eq!(first.read().unwrap().len(), 1);
87 }
88
89 #[test]
90 fn failed_update_leaves_persisted_policy_unchanged() {
91 let dir = tempfile::tempdir().unwrap();
92 let path = dir.path().join("peers.json");
93 std::fs::write(&path, "{}").unwrap();
94 let store = PolicyStore::open(&path).unwrap();
95 let alice = AgentKey::generate().unwrap().id().to_b64();
96 store.add("alice", &alice).unwrap();
97 let before = std::fs::read(&path).unwrap();
98 assert!(
99 store
100 .add("alice", &AgentKey::generate().unwrap().id().to_b64())
101 .is_err()
102 );
103 assert_eq!(std::fs::read(&path).unwrap(), before);
104 std::fs::remove_file(path.with_extension("json.lock")).unwrap();
106 std::fs::create_dir(path.with_extension("json.lock")).unwrap();
107 assert!(store.remove("alice").is_err());
108 assert!(store.read().unwrap().resolve("alice").is_ok());
109 }
110}