lean_ctx/core/policy/org/
trust.rs1use std::path::PathBuf;
9
10use serde::{Deserialize, Serialize};
11
12#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
13pub struct TrustedKey {
14 pub org: String,
15 pub public_key: String,
16 pub added_at: String,
17}
18
19#[derive(Clone, Debug, Default, Serialize, Deserialize)]
20pub struct TrustStore {
21 pub keys: Vec<TrustedKey>,
22}
23
24pub fn trust_path() -> Result<PathBuf, String> {
25 let dir = crate::core::paths::data_dir()?;
26 Ok(dir.join("org-trust.toml"))
27}
28
29pub(crate) fn trust_path_read_only() -> Result<PathBuf, String> {
30 Ok(crate::core::data_dir::resolve_data_dir()?.join("org-trust.toml"))
31}
32
33pub fn load() -> Result<TrustStore, String> {
34 let path = trust_path()?;
35 let Some(text) = crate::core::policy::files::read(&path, false)? else {
36 return Ok(TrustStore::default());
37 };
38 toml::from_str(&text).map_err(|_| "org trust input is invalid".into())
39}
40
41pub fn save(store: &TrustStore) -> Result<(), String> {
42 let path = trust_path()?;
43 if let Some(parent) = path.parent() {
44 std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {}: {e}", parent.display()))?;
45 }
46 let text = toml::to_string_pretty(store).map_err(|e| format!("serialize trust store: {e}"))?;
47 std::fs::write(&path, text).map_err(|e| format!("write {}: {e}", path.display()))
48}
49
50pub(crate) fn load_read_only() -> Result<TrustStore, String> {
54 let Some(text) = crate::core::policy::files::read(&trust_path_read_only()?, false)? else {
55 return Ok(TrustStore::default());
56 };
57 toml::from_str(&text).map_err(|_| "org trust input is invalid".into())
58}
59
60#[derive(Clone, Debug, Eq, PartialEq)]
62pub enum PinOutcome {
63 Added,
65 AlreadyPinned,
67 PinnedForAnother(String),
70}
71
72pub fn pin_for(org: &str, public_key: &str) -> Result<PinOutcome, String> {
74 let mut store = load()?;
75 if let Some(existing) = store.keys.iter().find(|k| k.public_key == public_key) {
76 return Ok(if existing.org == org {
77 PinOutcome::AlreadyPinned
78 } else {
79 PinOutcome::PinnedForAnother(existing.org.clone())
80 });
81 }
82 store.keys.push(TrustedKey {
83 org: org.to_string(),
84 public_key: public_key.to_string(),
85 added_at: chrono::Utc::now().to_rfc3339(),
86 });
87 save(&store)?;
88 Ok(PinOutcome::Added)
89}
90
91pub fn pin(org: &str, public_key: &str) -> Result<bool, String> {
94 pin_for(org, public_key).map(|outcome| outcome == PinOutcome::Added)
95}
96
97pub fn is_trusted_for(org: &str, public_key: &str) -> bool {
101 load_read_only()
102 .map(|store| {
103 store
104 .keys
105 .iter()
106 .any(|key| key.public_key == public_key && key.org == org)
107 })
108 .unwrap_or(false)
109}
110
111pub fn pinned_organization(public_key: &str) -> Option<String> {
113 load_read_only()
114 .ok()?
115 .keys
116 .into_iter()
117 .find(|key| key.public_key == public_key)
118 .map(|key| key.org)
119}
120
121pub fn remove(public_key: &str) -> Result<bool, String> {
122 let mut store = load()?;
123 let before = store.keys.len();
124 store.keys.retain(|k| k.public_key != public_key);
125 if store.keys.len() < before {
126 save(&store)?;
127 Ok(true)
128 } else {
129 Ok(false)
130 }
131}
132
133pub fn remove_org(org: &str) -> Result<Vec<String>, String> {
136 let mut store = load()?;
137 let (removed, kept): (Vec<TrustedKey>, Vec<TrustedKey>) =
138 store.keys.into_iter().partition(|k| k.org == org);
139 store.keys = kept;
140 if !removed.is_empty() {
141 save(&store)?;
142 }
143 Ok(removed.into_iter().map(|k| k.public_key).collect())
144}
145
146pub fn trusted_keys() -> Vec<TrustedKey> {
147 load().unwrap_or_default().keys
148}
149
150pub fn is_trusted(public_key: &str) -> bool {
151 load()
152 .map(|s| s.keys.iter().any(|k| k.public_key == public_key))
153 .unwrap_or(false)
154}
155
156pub fn any_pinned() -> bool {
157 load().map(|s| !s.keys.is_empty()).unwrap_or(false)
158}