clawft_kernel/licence/
approval_store.rs1use std::collections::BTreeMap;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex};
12
13use serde::{Deserialize, Serialize};
14
15use super::approval::{Approval, SignedApproval, verify_approval, verify_approval_signature};
16use super::persist::{read_capped, write_atomic};
17use super::{LicenceError, LocalMeshId, MAX_APPROVALS, Outcome};
18use crate::workload_pkg::TrustAnchors;
19
20pub const APPROVALS_FILE: &str = "checkout_approvals.json";
22
23#[derive(Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25struct ApprovalsFile {
26 v: u32,
27 approvals: Vec<SignedApproval>,
28}
29
30pub(super) type Held = BTreeMap<String, (SignedApproval, Approval)>;
32
33pub struct ApprovalStore {
35 path: PathBuf,
36 anchors: Arc<TrustAnchors>,
37 pub(super) local: LocalMeshId,
38 inner: Mutex<(Held, Option<String>)>,
39}
40
41impl ApprovalStore {
42 pub fn open(
45 dir: &Path,
46 anchors: Arc<TrustAnchors>,
47 local: LocalMeshId,
48 ) -> Result<Self, LicenceError> {
49 let path = dir.join(APPROVALS_FILE);
50 let held = load(&path, &anchors)?;
51 Ok(Self { path, anchors, local, inner: Mutex::new((held, None)) })
52 }
53
54 pub fn open_or_poisoned(dir: &Path, anchors: Arc<TrustAnchors>, local: LocalMeshId) -> Self {
57 let path = dir.join(APPROVALS_FILE);
58 let (held, poison) = match load(&path, &anchors) {
59 Ok(h) => (h, None),
60 Err(e) => (Held::new(), Some(e.to_string())),
61 };
62 Self { path, anchors, local, inner: Mutex::new((held, poison)) }
63 }
64
65 pub(super) fn lock(&self) -> std::sync::MutexGuard<'_, (Held, Option<String>)> {
66 self.inner.lock().unwrap_or_else(|p| p.into_inner())
67 }
68
69 pub fn poisoned(&self) -> Option<String> {
71 self.lock().1.clone()
72 }
73
74 pub fn accept(&self, signed: &SignedApproval) -> Result<Outcome, LicenceError> {
77 let mut g = self.lock();
78 if let Some(p) = &g.1 {
79 return Err(LicenceError::Poisoned(p.clone()));
80 }
81 let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
82 let approval = verify_approval(signed, &self.anchors, &local)?;
83 let key = approval.content_key();
84 if g.0.contains_key(&key) {
85 return Ok(Outcome::Duplicate);
86 }
87 if g.0.len() >= MAX_APPROVALS {
88 return Err(LicenceError::Full);
89 }
90 g.0.insert(key, (signed.clone(), approval));
91 save(&self.path, &g.0)?;
92 Ok(Outcome::Applied)
93 }
94
95 pub fn covering(&self, cog_id: &str, version: &str, sha256: &str) -> Option<String> {
98 let g = self.lock();
99 let local = self.local.get()?.to_hex();
100 if g.1.is_some() {
101 return None;
102 }
103 g.0.iter()
104 .find(|(_, (_, a))| {
105 a.mesh_id == local && a.cog_id == cog_id && a.version == version && a.covers(sha256)
106 })
107 .map(|(k, _)| k.clone())
108 }
109
110 pub fn orphaned(&self) -> Vec<String> {
112 let g = self.lock();
113 let local = self.local.get().map(|m| m.to_hex());
114 g.0.iter()
115 .filter(|(_, (_, a))| Some(&a.mesh_id) != local.as_ref())
116 .map(|(k, _)| k.clone())
117 .collect()
118 }
119
120 pub fn len(&self) -> usize {
122 self.lock().0.len()
123 }
124
125 pub fn is_empty(&self) -> bool {
127 self.len() == 0
128 }
129}
130
131fn save(path: &Path, held: &Held) -> Result<(), LicenceError> {
132 let file = ApprovalsFile { v: 1, approvals: held.values().map(|(s, _)| s.clone()).collect() };
133 let bytes = serde_json::to_vec(&file).map_err(|e| LicenceError::Persist(e.to_string()))?;
134 write_atomic(path, &bytes)
135}
136
137fn load(path: &Path, anchors: &TrustAnchors) -> Result<Held, LicenceError> {
138 let Some(bytes) = read_capped(path)? else {
139 return Ok(Held::new());
140 };
141 let file: ApprovalsFile =
142 serde_json::from_slice(&bytes).map_err(|e| LicenceError::Malformed(e.to_string()))?;
143 if file.v != 1 || file.approvals.len() > MAX_APPROVALS {
144 return Err(LicenceError::Malformed("approvals version or size".into()));
145 }
146 let mut held = Held::new();
147 for s in file.approvals {
148 let a = verify_approval_signature(&s, anchors)?;
149 held.insert(a.content_key(), (s, a));
150 }
151 Ok(held)
152}