1use std::collections::BTreeMap;
20use std::path::Path;
21
22use anyhow::{Context, Result, bail};
23use serde::{Deserialize, Serialize};
24
25use crate::identity::AgentId;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
30struct RawPeer {
31 key: String,
32}
33
34#[derive(Debug, Clone)]
36pub struct Peer {
37 pub petname: String,
38 pub id: AgentId,
39}
40
41#[derive(Debug, Clone, Default)]
44pub struct Policy {
45 peers: Vec<Peer>,
46}
47
48impl Policy {
49 pub fn load(path: &Path) -> Result<Self> {
50 let raw = std::fs::read_to_string(path)
51 .with_context(|| format!("reading peers file {}", path.display()))?;
52 Self::parse(&raw)
53 }
54
55 pub fn parse(raw: &str) -> Result<Self> {
56 let map: BTreeMap<String, RawPeer> =
57 serde_json::from_str(raw).context("peers file is not valid policy JSON")?;
58 let mut peers = Vec::with_capacity(map.len());
59 for (petname, rp) in map {
60 let id = AgentId::from_b64(&rp.key)
61 .with_context(|| format!("peer '{petname}' has an invalid key"))?;
62 peers.push(Peer { petname, id });
63 }
64 for i in 0..peers.len() {
67 for j in (i + 1)..peers.len() {
68 if peers[i].id == peers[j].id {
69 bail!(
70 "peers '{}' and '{}' share the same key",
71 peers[i].petname,
72 peers[j].petname
73 );
74 }
75 }
76 }
77 Ok(Self { peers })
78 }
79
80 pub fn peer(&self, id: AgentId) -> Option<&Peer> {
82 self.peers.iter().find(|p| p.id == id)
83 }
84
85 pub fn resolve(&self, petname: &str) -> Result<AgentId> {
87 self.peers
88 .iter()
89 .find(|p| p.petname == petname)
90 .map(|p| p.id)
91 .with_context(|| format!("unknown peer '{petname}'"))
92 }
93
94 pub fn len(&self) -> usize {
95 self.peers.len()
96 }
97
98 pub fn is_empty(&self) -> bool {
99 self.peers.is_empty()
100 }
101
102 pub fn peers(&self) -> &[Peer] {
104 &self.peers
105 }
106
107 pub fn add(&mut self, petname: &str, key_b64: &str) -> Result<()> {
112 let id = AgentId::from_b64(key_b64)
113 .with_context(|| format!("peer '{petname}' has an invalid key"))?;
114 if let Some(other) = self
115 .peers
116 .iter()
117 .find(|p| p.id == id && p.petname != petname)
118 {
119 bail!("that key is already authorized as '{}'", other.petname);
120 }
121 match self.peers.iter().find(|p| p.petname == petname) {
122 Some(existing) if existing.id == id => {}
126 Some(existing) => bail!(
127 "petname '{petname}' is already authorized under a different key ({}); \
128 remove it first to reassign",
129 existing.id.to_b64()
130 ),
131 None => self.peers.push(Peer {
132 petname: petname.to_string(),
133 id,
134 }),
135 }
136 Ok(())
137 }
138
139 pub fn remove(&mut self, petname: &str) -> bool {
141 let before = self.peers.len();
142 self.peers.retain(|p| p.petname != petname);
143 self.peers.len() != before
144 }
145
146 pub fn to_json(&self) -> Result<String> {
148 let map: BTreeMap<String, RawPeer> = self
149 .peers
150 .iter()
151 .map(|p| (p.petname.clone(), RawPeer { key: p.id.to_b64() }))
152 .collect();
153 serde_json::to_string_pretty(&map).context("serializing peers")
154 }
155
156 pub fn save(&self, path: &Path) -> Result<()> {
158 let mut json = self.to_json()?;
159 json.push('\n');
160 std::fs::write(path, json).with_context(|| format!("writing {}", path.display()))?;
161 Ok(())
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::identity::AgentKey;
169
170 fn policy_with_key(k: &AgentKey) -> Policy {
171 let raw = format!(r#"{{ "alice": {{ "key": "{}" }} }}"#, k.id().to_b64());
172 Policy::parse(&raw).unwrap()
173 }
174
175 #[test]
176 fn listed_key_resolves_to_its_petname() {
177 let k = AgentKey::generate().unwrap();
178 let p = policy_with_key(&k);
179 assert_eq!(p.peer(k.id()).unwrap().petname, "alice");
180 }
181
182 #[test]
183 fn legacy_may_field_is_ignored() {
184 let k = AgentKey::generate().unwrap();
186 let raw = format!(
187 r#"{{ "alice": {{ "key": "{}", "may": "*" }} }}"#,
188 k.id().to_b64()
189 );
190 let p = Policy::parse(&raw).unwrap();
191 assert_eq!(p.peer(k.id()).unwrap().petname, "alice");
192 }
193
194 #[test]
195 fn unlisted_sender_is_denied() {
196 let k = AgentKey::generate().unwrap();
197 let p = policy_with_key(&k);
198 let stranger = AgentKey::generate().unwrap();
199 assert!(
200 p.peer(stranger.id()).is_none(),
201 "unlisted key must not resolve"
202 );
203 }
204
205 #[test]
206 fn invalid_key_fails_at_parse_time() {
207 let raw = r#"{ "alice": { "key": "not-a-real-key" } }"#;
208 assert!(Policy::parse(raw).is_err());
209 }
210
211 #[test]
212 fn duplicate_keys_are_rejected() {
213 let k = AgentKey::generate().unwrap();
214 let raw = format!(
215 r#"{{ "a": {{ "key": "{0}" }}, "b": {{ "key": "{0}" }} }}"#,
216 k.id().to_b64()
217 );
218 assert!(Policy::parse(&raw).is_err());
219 }
220
221 #[test]
222 fn resolve_maps_petname_to_key() {
223 let k = AgentKey::generate().unwrap();
224 let p = policy_with_key(&k);
225 assert_eq!(p.resolve("alice").unwrap(), k.id());
226 assert!(p.resolve("nobody").is_err());
227 }
228
229 #[test]
230 fn add_then_resolve_and_persist_round_trip() {
231 let k = AgentKey::generate().unwrap();
232 let mut p = Policy::default();
233 p.add("desktop", &k.id().to_b64()).unwrap();
234 assert_eq!(p.resolve("desktop").unwrap(), k.id());
235 let reparsed = Policy::parse(&p.to_json().unwrap()).unwrap();
236 assert_eq!(reparsed.resolve("desktop").unwrap(), k.id());
237 }
238
239 #[test]
240 fn add_same_petname_is_idempotent() {
241 let k = AgentKey::generate().unwrap();
242 let mut p = Policy::default();
243 p.add("bot", &k.id().to_b64()).unwrap();
244 p.add("bot", &k.id().to_b64()).unwrap();
245 assert_eq!(p.len(), 1, "same petname does not duplicate");
246 }
247
248 #[test]
249 fn add_refuses_to_rekey_an_existing_petname() {
250 let old = AgentKey::generate().unwrap();
252 let attacker = AgentKey::generate().unwrap();
253 let mut p = Policy::default();
254 p.add("laptop", &old.id().to_b64()).unwrap();
255 assert!(
256 p.add("laptop", &attacker.id().to_b64()).is_err(),
257 "an existing petname must not be silently rekeyed"
258 );
259 assert_eq!(
260 p.resolve("laptop").unwrap(),
261 old.id(),
262 "the original binding is preserved"
263 );
264 }
265
266 #[test]
267 fn add_rejects_key_under_a_second_petname() {
268 let k = AgentKey::generate().unwrap();
269 let mut p = Policy::default();
270 p.add("first", &k.id().to_b64()).unwrap();
271 assert!(
272 p.add("second", &k.id().to_b64()).is_err(),
273 "one key must not get two petnames"
274 );
275 }
276
277 #[test]
278 fn add_rejects_invalid_key() {
279 let mut p = Policy::default();
280 assert!(p.add("x", "not-a-key").is_err());
281 }
282
283 #[test]
284 fn remove_reports_whether_it_removed() {
285 let k = AgentKey::generate().unwrap();
286 let mut p = Policy::default();
287 p.add("gone", &k.id().to_b64()).unwrap();
288 assert!(p.remove("gone"));
289 assert!(!p.remove("gone"), "already absent");
290 assert!(p.is_empty());
291 }
292}