use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
use super::floor::FloorState;
use super::persist::write_atomic;
use super::store_load::{SlotFile, StoreFile, load};
use super::{
BindState, BindingRecord, CheckoutGrant, Clock, GRANT_SKEW_SECS, GrantArtifact, LicenceError,
LicenceEvent, LicenceEventSink, LocalMeshId, NoopSink, Outcome, SignedEnvelope,
};
use crate::revocation::{RevocationKind, RevocationList};
use crate::workload_pkg::TrustAnchors;
pub const GRANTS_FILE: &str = "checkout_grants.json";
const HW_PERSIST_STEP: u64 = 60;
#[derive(Clone)]
pub(super) struct Held<T> {
pub(super) signed: SignedEnvelope,
pub(super) body: T,
}
#[derive(Default, Clone)]
pub(super) struct Slot {
pub(super) current: Option<Held<CheckoutGrant>>,
pub(super) previous: Option<Held<CheckoutGrant>>,
pub(super) conflicted: Vec<u64>,
}
#[derive(Default, Clone)]
pub(super) struct Inner {
pub(super) binding: Option<Held<BindingRecord>>,
pub(super) slots: BTreeMap<(String, String), Slot>,
pub(super) floors: BTreeMap<String, FloorState>,
pub(super) persisted_hw: u64,
pub(super) orphan_reported: bool,
pub(super) dirty: bool,
pub(super) poisoned: Option<String>,
}
#[derive(Debug, Clone)]
pub struct VerifiedCheckoutGrant {
grant: CheckoutGrant,
grant_pubkey_hex: String,
}
impl VerifiedCheckoutGrant {
#[cfg(test)]
pub(super) fn for_test(grant: CheckoutGrant) -> Self {
let pk = super::tests_common::pk_hex(&super::tests_common::grant_key());
Self { grant, grant_pubkey_hex: pk }
}
pub fn grant(&self) -> &CheckoutGrant {
&self.grant
}
pub fn grant_pubkey_hex(&self) -> &str {
&self.grant_pubkey_hex
}
}
pub struct CheckoutGrantStore {
pub(super) path: PathBuf,
pub(super) anchors: Arc<TrustAnchors>,
pub(super) local: LocalMeshId,
pub(super) clock: Clock,
pub(super) sink: RwLock<Arc<dyn LicenceEventSink>>,
pub(super) revocations: RwLock<Option<Arc<RevocationList>>>,
pub(super) inner: Mutex<Inner>,
}
impl std::fmt::Debug for CheckoutGrantStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CheckoutGrantStore").finish_non_exhaustive()
}
}
pub(super) fn grant_valid(g: &CheckoutGrant, eff_now: u64) -> bool {
!g.is_withdrawal()
&& eff_now < g.expires_at
&& eff_now < g.licence.expires
&& g.issued_at <= eff_now.saturating_add(GRANT_SKEW_SECS)
}
impl CheckoutGrantStore {
pub fn open(
dir: &Path,
anchors: Arc<TrustAnchors>,
local: LocalMeshId,
clock: Clock,
) -> Result<Self, LicenceError> {
let path = dir.join(GRANTS_FILE);
let inner = load(&path, &anchors)?;
Ok(Self::build(path, anchors, local, clock, inner))
}
pub fn open_or_poisoned(
dir: &Path,
anchors: Arc<TrustAnchors>,
local: LocalMeshId,
clock: Clock,
) -> Self {
let path = dir.join(GRANTS_FILE);
let inner = load(&path, &anchors).unwrap_or_else(|e| Inner {
poisoned: Some(e.to_string()),
..Inner::default()
});
Self::build(path, anchors, local, clock, inner)
}
fn build(
path: PathBuf,
anchors: Arc<TrustAnchors>,
local: LocalMeshId,
clock: Clock,
inner: Inner,
) -> Self {
Self {
path,
anchors,
local,
clock,
sink: RwLock::new(Arc::new(NoopSink)),
revocations: RwLock::new(None),
inner: Mutex::new(inner),
}
}
pub fn set_sink(&self, sink: Arc<dyn LicenceEventSink>) {
*self.sink.write().unwrap_or_else(|p| p.into_inner()) = sink;
}
pub fn attach_revocations(&self, list: Arc<RevocationList>) {
*self.revocations.write().unwrap_or_else(|p| p.into_inner()) = Some(list);
}
pub fn local_mesh_id(&self) -> &LocalMeshId {
&self.local
}
pub fn poisoned(&self) -> Option<String> {
self.lock().poisoned.clone()
}
pub(super) fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(|p| p.into_inner())
}
fn emit(&self, ev: LicenceEvent) {
self.sink.read().unwrap_or_else(|p| p.into_inner()).emit(ev);
}
pub(super) fn run<T>(&self, f: impl FnOnce(&mut Inner, &mut Vec<LicenceEvent>) -> T) -> T {
let mut events = Vec::new();
let out = {
let mut g = self.lock();
f(&mut g, &mut events)
};
for e in events {
self.emit(e);
}
out
}
pub(super) fn key_revoked(&self, pk_hex: &str) -> bool {
let g = self.revocations.read().unwrap_or_else(|p| p.into_inner());
g.as_ref()
.is_some_and(|l| l.is_subject_revoked(RevocationKind::SignerKey, pk_hex))
}
pub fn is_hash_revoked(&self, blake3_hex: &str) -> bool {
let g = self.revocations.read().unwrap_or_else(|p| p.into_inner());
g.as_ref()
.is_some_and(|l| l.is_subject_revoked(RevocationKind::ArtifactHash, blake3_hex))
}
fn save(&self, inner: &mut Inner) -> Result<(), LicenceError> {
let file = StoreFile {
v: 1,
binding: inner.binding.as_ref().map(|b| b.signed.clone()),
grants: inner
.slots
.iter()
.map(|((cog_id, version), s)| SlotFile {
cog_id: cog_id.clone(),
version: version.clone(),
current: s.current.as_ref().map(|h| h.signed.clone()),
previous: s.previous.as_ref().map(|h| h.signed.clone()),
conflicted: s.conflicted.clone(),
})
.collect(),
floors: inner.floors.clone(),
};
let bytes =
serde_json::to_vec(&file).map_err(|e| LicenceError::Persist(e.to_string()))?;
write_atomic(&self.path, &bytes)?;
inner.persisted_hw = inner.floors.values().map(|f| f.hw).max().unwrap_or(0);
inner.dirty = false;
Ok(())
}
pub(super) fn commit(&self, inner: &mut Inner, mut next: Inner) -> Result<(), LicenceError> {
self.save(&mut next)?;
*inner = next;
Ok(())
}
pub(super) fn commit_restrictive(&self, inner: &mut Inner, mut next: Inner) -> Outcome {
let saved = self.save(&mut next);
next.dirty = saved.is_err();
*inner = next;
match saved {
Ok(()) => Outcome::Applied,
Err(e) => {
tracing::warn!(error = %e, "restrictive licence record applied but not saved; tick retries");
Outcome::AppliedUnsaved
}
}
}
pub(super) fn binding_in_effect(
&self,
inner: &mut Inner,
ev: &mut Vec<LicenceEvent>,
) -> Result<BindingRecord, LicenceError> {
if let Some(p) = &inner.poisoned {
return Err(LicenceError::Poisoned(p.clone()));
}
let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
let b = inner.binding.as_ref().ok_or(LicenceError::NoBinding)?.body.clone();
if b.mesh_id != local.to_hex() {
if !inner.orphan_reported {
inner.orphan_reported = true;
ev.push(LicenceEvent::BindingOrphaned {
stored: b.mesh_id.clone(),
local: local.to_hex(),
});
}
return Err(LicenceError::Orphaned);
}
inner.orphan_reported = false;
if b.state != BindState::Bound {
return Err(LicenceError::Unbound);
}
Ok(b)
}
pub(super) fn eff_now(&self, inner: &mut Inner, key: &str) -> u64 {
let now = (self.clock)();
let fs = inner.floors.entry(key.to_owned()).or_default();
fs.observe(now);
fs.effective_now(now)
}
pub fn active_binding(&self) -> Option<BindingRecord> {
self.binding_status().ok()
}
pub fn binding_status(&self) -> Result<BindingRecord, LicenceError> {
self.run(|inner, ev| self.binding_in_effect(inner, ev))
}
pub fn held_binding(&self) -> Option<BindingRecord> {
self.lock().binding.as_ref().map(|h| h.body.clone())
}
pub fn verify_member(&self, signed: &super::SignedBinding) -> Result<BindingRecord, LicenceError> {
let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
super::verify_binding_member(signed, &self.anchors, &local)
}
fn with_valid<T>(&self, mut f: impl FnMut(&CheckoutGrant, &str) -> Option<T>) -> Option<T> {
self.run(|inner, ev| {
let b = self.binding_in_effect(inner, ev).ok()?;
if self.key_revoked(&b.grant_pubkey) {
return None;
}
let eff = self.eff_now(inner, &b.grant_pubkey);
inner
.slots
.values()
.filter_map(|s| s.current.as_ref())
.filter(|h| h.body.mesh_id == b.mesh_id && grant_valid(&h.body, eff))
.find_map(|h| f(&h.body, &b.grant_pubkey))
})
}
pub fn valid_grant_covering(
&self,
blake3_hex: &str,
cog_id: &str,
version: &str,
) -> Option<CheckoutGrant> {
self.with_valid(|g, _| {
(g.cog_id == cog_id && g.version == version && g.artifact_by_blake3(blake3_hex).is_some())
.then(|| g.clone())
})
}
pub fn valid_grant_for_artifact(
&self,
cog_id: &str,
version: &str,
sha256: &str,
blake3: &str,
) -> Option<(CheckoutGrant, GrantArtifact)> {
self.with_valid(|g, _| {
if g.cog_id != cog_id || g.version != version {
return None;
}
g.artifacts
.iter()
.find(|a| a.sha256 == sha256 && a.blake3 == blake3)
.map(|a| (g.clone(), a.clone()))
})
}
pub fn verified_grants(&self) -> Vec<VerifiedCheckoutGrant> {
let mut out = Vec::new();
self.with_valid::<()>(|g, pk| {
out.push(VerifiedCheckoutGrant { grant: g.clone(), grant_pubkey_hex: pk.to_owned() });
None
});
out
}
pub fn floor(&self) -> Option<u64> {
self.run(|inner, ev| {
let b = self.binding_in_effect(inner, ev).ok()?;
self.eff_now(inner, &b.grant_pubkey);
inner.floors.get(&b.grant_pubkey).map(FloorState::floor)
})
}
pub fn reset_floor(&self) -> Result<(), LicenceError> {
self.run(|inner, ev| {
let b = self.binding_in_effect(inner, ev)?;
let now = (self.clock)();
let mut next = inner.clone();
next.floors.entry(b.grant_pubkey).or_default().reset(now);
ev.push(LicenceEvent::FloorReset(now));
self.commit(inner, next)
})
}
pub fn tick(&self) {
self.run(|inner, ev| {
if inner.dirty && inner.poisoned.is_none() {
let _ = self.save(inner);
}
if let Ok(b) = self.binding_in_effect(inner, ev) {
self.eff_now(inner, &b.grant_pubkey);
let hw = inner.floors.get(&b.grant_pubkey).map_or(0, |f| f.hw);
if hw >= inner.persisted_hw.saturating_add(HW_PERSIST_STEP) {
let _ = self.save(inner);
}
}
})
}
}