plugin/host/src/meter.rs
//! **The Meter kind, host side** — [`HandlerRuntime`]'s sibling, with the same
//! two implementations for the same reason: one guest implementation compiles
//! native and to `wasm32-unknown-unknown`, and the host cannot tell them apart.
//!
//! [`HandlerRuntime`]: crate::backend::HandlerRuntime
//!
//! # What is new here and is not in the handler lane
//!
//! A package handler gets the store, always, implicitly. A meter gets **only
//! what an operator granted it**, and the grant is enforced twice:
//!
//! * [`GrantTable`] answers what a module was given, from a config file. An
//! unreadable table refuses **every** module — gunnar's GUN-007 rule, adopted
//! verbatim, because a gate whose configuration cannot be read is a gate that
//! is not standing.
//! * [`GrantedHost`] is the only bridge a guest reaches the host through, and
//! every method on it checks the grant before it does anything. A capability
//! that was not granted answers a **named refusal**, never a zero.
//!
//! For a wasm guest there is a third, stronger enforcement in
//! [`crate::meter_wasm`]: the linker defines only the granted imports, so an
//! ungranted capability is not merely refused — the module does not instantiate.
use std::collections::BTreeMap;
use std::sync::Arc;
use holger_plugin_abi::v2::{
Capability, DigestState, GuestKind, Meter, MeterHost, WireAuditRecord, WireCallContext,
WireCapacity, WireFactSummary, WireInstant, WireManifest2, WireMeterDecl, WireMeterReading,
WireSelftest, WireVerdict,
};
use crate::backend::SharedStore;
/// What the host lends a meter guest beyond the store: the counters, the audit
/// log and the clock. holger's server implements this; the plugin host never
/// counts anything itself.
///
/// `audit_write` takes only the guest's record. The host adds the time, the
/// identity and the source address from the call it is already inside — which is
/// why [`WireAuditRecord`] has no fields for them.
pub trait MeterHostServices: Send + Sync {
fn usage_read(&self, repository: &str) -> Result<Vec<WireMeterReading>, String>;
fn audit_write(&self, record: &WireAuditRecord) -> Result<(), String>;
fn clock(&self) -> Result<WireInstant, String>;
}
/// A services implementation that refuses everything, by name. Used by a
/// deployment that grants no meter anything, and by tests that must prove a
/// guest does not secretly depend on a capability.
pub struct NoServices;
impl MeterHostServices for NoServices {
fn usage_read(&self, _repository: &str) -> Result<Vec<WireMeterReading>, String> {
Err("this holger has no usage source wired".to_string())
}
fn audit_write(&self, _record: &WireAuditRecord) -> Result<(), String> {
Err("this holger has no audit sink wired".to_string())
}
fn clock(&self) -> Result<WireInstant, String> {
Err("this holger has no trusted clock wired".to_string())
}
}
/// **The operator's grant, per module.**
///
/// It comes from holger's config file and never from an environment variable:
/// the appliance has no shell, so an env var is a decision nobody can make and
/// nobody can read back.
#[derive(Debug, Clone)]
pub struct GrantTable {
entries: BTreeMap<String, Vec<Capability>>,
/// `Some(reason)` when the table could not be read or parsed. Every lookup
/// then refuses, naming the reason.
unreadable: Option<String>,
}
impl Default for GrantTable {
/// The empty table: every module present on disk gets nothing. A module
/// dropped in without an operator decision can do nothing at all.
fn default() -> Self {
Self {
entries: BTreeMap::new(),
unreadable: None,
}
}
}
impl GrantTable {
/// Build from `(module file name, capability names)` pairs as the config
/// file spells them. An unknown capability name is refused **by name**
/// rather than skipped, because a typo that silently grants less produces a
/// module that refuses at runtime for a reason nobody can find.
pub fn from_pairs<I, S, T>(pairs: I) -> Result<Self, String>
where
I: IntoIterator<Item = (S, Vec<T>)>,
S: Into<String>,
T: AsRef<str>,
{
let mut entries = BTreeMap::new();
for (module, names) in pairs {
let module = module.into();
let mut caps = Vec::new();
for n in names {
let n = n.as_ref();
let cap = Capability::from_name(n).ok_or_else(|| {
format!(
"plugin grant for `{module}` names `{n}`, which is not a holger \
capability. The capabilities are: {}",
Capability::ALL
.iter()
.map(|c| c.name())
.collect::<Vec<_>>()
.join(", ")
)
})?;
if !caps.contains(&cap) {
caps.push(cap);
}
}
caps.sort();
if entries.insert(module.clone(), caps).is_some() {
return Err(format!(
"plugin grants name `{module}` twice; one module, one decision"
));
}
}
Ok(Self {
entries,
unreadable: None,
})
}
/// GUN-007: an unreadable or unparseable grant file refuses every module.
pub fn unreadable(reason: impl Into<String>) -> Self {
Self {
entries: BTreeMap::new(),
unreadable: Some(reason.into()),
}
}
/// What this module was granted. An absent entry is the **empty** grant, not
/// a default one.
pub fn for_module(&self, module: &str) -> Result<&[Capability], String> {
if let Some(reason) = &self.unreadable {
return Err(format!(
"the plugin grant configuration could not be read ({reason}), so no module is \
granted anything — including `{module}`. A gate whose configuration cannot be \
read is not standing."
));
}
Ok(self
.entries
.get(module)
.map(|v| v.as_slice())
.unwrap_or(&[]))
}
/// The whole effective table, for the module census `AdminService.ServerProfile`
/// reports and for the open-build proof that reads it.
pub fn effective(&self) -> BTreeMap<String, Vec<&'static str>> {
self.entries
.iter()
.map(|(k, v)| (k.clone(), v.iter().map(|c| c.name()).collect()))
.collect()
}
/// Every capability granted to anything on this boot. The open-build proof
/// asserts this set holds none of `principal.read`, `digest.read`,
/// `clock.read`, `audit.write` — which proves "and none can be", from
/// configuration, without looking at a directory at all.
pub fn every_granted_capability(&self) -> Vec<Capability> {
let mut out: Vec<Capability> = self.entries.values().flatten().copied().collect();
out.sort();
out.dedup();
out
}
}
/// The bridge a guest reaches the host through, with the grant checked on every
/// method. The native half of the enforcement; the wasm half is the linker.
pub struct GrantedHost {
store: SharedStore,
services: Arc<dyn MeterHostServices>,
granted: Vec<Capability>,
}
impl GrantedHost {
pub fn new(
store: SharedStore,
services: Arc<dyn MeterHostServices>,
granted: &[Capability],
) -> Self {
Self {
store,
services,
granted: granted.to_vec(),
}
}
fn check(&self, cap: Capability) -> Result<(), String> {
if self.granted.contains(&cap) {
Ok(())
} else {
Err(format!(
"capability `{}` is not granted to this module",
cap.name()
))
}
}
}
impl MeterHost for GrantedHost {
fn store_get(&self, key: &str) -> Result<Option<Vec<u8>>, String> {
self.check(Capability::StoreRead)?;
self.store.get(key)
}
fn store_put(&self, key: &str, data: &[u8]) -> Result<(), String> {
self.check(Capability::StoreWrite)?;
self.store.put(key, data)
}
fn store_list(&self, prefix: &str) -> Result<Vec<(String, u64)>, String> {
self.check(Capability::StoreList)?;
self.store.list(prefix)
}
fn store_size(&self, key: &str) -> Result<Option<u64>, String> {
self.check(Capability::StoreRead)?;
self.store.size(key)
}
fn usage_read(&self, repository: &str) -> Result<Vec<WireMeterReading>, String> {
self.check(Capability::UsageRead)?;
self.services.usage_read(repository)
}
fn audit_write(&self, record: &WireAuditRecord) -> Result<(), String> {
self.check(Capability::AuditWrite)?;
self.services.audit_write(record)
}
fn clock(&self) -> Result<WireInstant, String> {
self.check(Capability::ClockRead)?;
self.services.clock()
}
}
/// Strip from a call context every field the module was not granted.
///
/// This is the host's half of `principal.read` and `digest.read`, which are
/// capabilities over FIELDS rather than over imports. It is a separate function
/// and not an inline `if` at the call site, because there will be more than one
/// call site and a field that leaks once has leaked.
///
/// A stripped digest becomes [`DigestState::NotGranted`], which is deliberately
/// distinct from [`DigestState::NotKnown`]: a guest must be able to tell "you
/// may not see it" from "the backend cannot produce one".
pub fn redact_context(mut ctx: WireCallContext, granted: &[Capability]) -> WireCallContext {
if !granted.contains(&Capability::PrincipalRead) {
ctx.principal = None;
}
if !granted.contains(&Capability::DigestRead) {
if let Some(a) = ctx.artifact.as_mut() {
a.digest = None;
a.digest_state = DigestState::NotGranted;
}
}
if !granted.contains(&Capability::ClockRead) {
ctx.now = None;
}
ctx
}
/// One meter guest, with its store and services already bound — however it is
/// executed. The sibling of `HandlerRuntime`, and deliberately the same shape.
pub trait MeterRuntime: Send + Sync {
fn manifest(&self) -> &WireManifest2;
/// What the operator granted this module. Reported by the census.
fn granted(&self) -> &[Capability];
fn meters(&self) -> Result<Vec<WireMeterDecl>, String>;
fn read(&self, repository: &str) -> Result<Vec<WireMeterReading>, String>;
fn capacity(&self) -> Result<WireCapacity, String>;
fn selftest(&self) -> Result<WireSelftest, String>;
fn evaluate(&self, ctx: &WireCallContext) -> Result<WireVerdict, String>;
fn accept_fact(&self, signed: &[u8]) -> Result<WireFactSummary, String>;
fn describe_fact(&self, repository: &str) -> Result<WireFactSummary, String>;
}
/// A meter compiled into the server, bound to a store and a grant. The native
/// half of "one implementation, two targets".
pub struct LinkedMeter<M: Meter + Send + Sync> {
meter: M,
host: GrantedHost,
manifest: WireManifest2,
granted: Vec<Capability>,
}
impl<M: Meter + Send + Sync> LinkedMeter<M> {
/// Bind a native meter. The grant is checked **here**, against the same
/// `check_grants` a loaded module goes through, so a linked meter cannot get
/// more than a loaded one would.
pub fn new(
name: &str,
meter: M,
store: SharedStore,
services: Arc<dyn MeterHostServices>,
granted: &[Capability],
) -> Result<Self, String> {
let manifest = meter.manifest();
if manifest.kind != GuestKind::Meter {
return Err(format!(
"{name}: declares kind `{}`, which is not a meter",
manifest.kind.name()
));
}
// A linked meter has no import table to read, so gate B has nothing to
// compare — but gate A and the kind floor apply unchanged.
holger_plugin_abi::v2::check_grants(name, &manifest, &[], granted)
.map_err(|e| e.to_string())?;
let host = GrantedHost::new(store, services, granted);
Ok(Self {
meter,
host,
manifest,
granted: granted.to_vec(),
})
}
}
impl<M: Meter + Send + Sync> std::fmt::Debug for LinkedMeter<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LinkedMeter")
.field("name", &self.manifest.name)
.field(
"granted",
&self.granted.iter().map(|c| c.name()).collect::<Vec<_>>(),
)
.finish()
}
}
impl<M: Meter + Send + Sync> MeterRuntime for LinkedMeter<M> {
fn manifest(&self) -> &WireManifest2 {
&self.manifest
}
fn granted(&self) -> &[Capability] {
&self.granted
}
fn meters(&self) -> Result<Vec<WireMeterDecl>, String> {
self.meter.meters()
}
fn read(&self, repository: &str) -> Result<Vec<WireMeterReading>, String> {
self.meter.read(&self.host, repository)
}
fn capacity(&self) -> Result<WireCapacity, String> {
self.meter.capacity(&self.host)
}
fn selftest(&self) -> Result<WireSelftest, String> {
self.meter.selftest(&self.host)
}
fn evaluate(&self, ctx: &WireCallContext) -> Result<WireVerdict, String> {
let ctx = redact_context(ctx.clone(), &self.granted);
self.meter.evaluate(&self.host, &ctx)
}
fn accept_fact(&self, signed: &[u8]) -> Result<WireFactSummary, String> {
self.meter.accept_fact(&self.host, signed)
}
fn describe_fact(&self, repository: &str) -> Result<WireFactSummary, String> {
self.meter.describe_fact(&self.host, repository)
}
}
/// One row of the module census `AdminService.ServerProfile` reports.
///
/// On an appliance with no shell this is the only way an operator can ask *"what
/// is loaded here, and what may it do"* — and it is the only way the open-build
/// proof can ask either.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModuleCensusRow {
/// The module's own name from its manifest.
pub name: String,
/// `linked` for a compiled-in guest, or the module file name for a loaded one.
pub origin: String,
pub kind: &'static str,
/// Hex blake3-256 of the module bytes; empty for a linked guest, which has no
/// module bytes to digest.
pub digest: String,
/// The import receipt this module arrived under, when it arrived as one.
pub receipt: Option<String>,
pub granted: Vec<&'static str>,
}
#[cfg(test)]
mod tests {
use super::*;
use holger_plugin_abi::v2::{
AbiRange, ClockSource, DigestAlgo, Enforcement, MeterFacts, MeterScope, ReadingValue,
WireArtifactRef, WireDigest, WirePrincipal, WireVerb,
};
use holger_plugin_abi::wire::WireArtifactId;
use holger_plugin_abi::BlobStore;
struct EmptyStore;
impl BlobStore for EmptyStore {
fn get(&self, _k: &str) -> Result<Option<Vec<u8>>, String> {
Ok(None)
}
fn put(&self, _k: &str, _d: &[u8]) -> Result<(), String> {
Ok(())
}
fn list(&self, _p: &str) -> Result<Vec<(String, u64)>, String> {
Ok(vec![("a".into(), 10)])
}
}
struct CountingServices;
impl MeterHostServices for CountingServices {
fn usage_read(&self, _repository: &str) -> Result<Vec<WireMeterReading>, String> {
Ok(vec![WireMeterReading {
meter: "storage_total_bytes".into(),
scope: MeterScope::Product,
enforcement: Enforcement::Measured,
value: ReadingValue::Counted(1),
}])
}
fn audit_write(&self, _record: &WireAuditRecord) -> Result<(), String> {
Ok(())
}
fn clock(&self) -> Result<WireInstant, String> {
Ok(WireInstant {
unix_nanos: 1,
source: ClockSource::MonotonicSinceBoot,
trusted_until_unix_nanos: 0,
})
}
}
fn host(granted: &[Capability]) -> GrantedHost {
GrantedHost::new(Arc::new(EmptyStore), Arc::new(CountingServices), granted)
}
#[test]
fn an_ungranted_capability_is_a_named_refusal_not_a_zero() {
let h = host(&[Capability::UsageRead]);
assert!(h.usage_read("r").is_ok());
assert!(h.store_list("").unwrap_err().contains("store.list"));
assert!(h
.audit_write(&WireAuditRecord::default())
.unwrap_err()
.contains("audit.write"));
assert!(h.clock().unwrap_err().contains("clock.read"));
assert!(h.store_put("k", b"v").unwrap_err().contains("store.write"));
}
#[test]
fn an_unreadable_grant_table_refuses_every_module_by_name() {
let t = GrantTable::unreadable("/data/holger.ron: permission denied");
let err = t.for_module("allocation.wasm").unwrap_err();
assert!(err.contains("permission denied"), "{err}");
assert!(err.contains("allocation.wasm"), "{err}");
assert!(t.every_granted_capability().is_empty());
}
#[test]
fn a_module_with_no_entry_gets_the_empty_grant() {
let t = GrantTable::from_pairs([("allocation.wasm", vec!["usage.read"])]).unwrap();
assert_eq!(t.for_module("stranger.wasm").unwrap(), &[] as &[Capability]);
assert_eq!(
t.for_module("allocation.wasm").unwrap(),
&[Capability::UsageRead]
);
}
#[test]
fn a_typo_in_the_grant_file_is_refused_by_name_and_lists_the_vocabulary() {
let err = GrantTable::from_pairs([("m.wasm", vec!["usage.reed"])]).unwrap_err();
assert!(err.contains("usage.reed"), "{err}");
assert!(err.contains("usage.read"), "{err}");
}
#[test]
fn one_module_one_decision() {
let err = GrantTable::from_pairs([
("m.wasm", vec!["usage.read"]),
("m.wasm", vec!["store.list"]),
])
.unwrap_err();
assert!(err.contains("twice"), "{err}");
}
#[test]
fn the_census_reads_the_effective_table() {
let t = GrantTable::from_pairs([("allocation.wasm", vec!["usage.read", "store.list"])])
.unwrap();
assert_eq!(
t.effective().get("allocation.wasm").unwrap(),
&vec!["store.list", "usage.read"]
);
assert_eq!(
t.every_granted_capability(),
vec![Capability::StoreList, Capability::UsageRead]
);
}
fn ctx() -> WireCallContext {
WireCallContext {
abi: 2,
repository: "releases".into(),
verb: WireVerb::Put,
principal: Some(WirePrincipal {
ident: "sub-123".into(),
method: "oidc".into(),
source_ip: "10.0.0.1:443".into(),
}),
artifact: Some(WireArtifactRef {
id: WireArtifactId {
namespace: None,
name: "a".into(),
version: "1".into(),
},
digest: Some(WireDigest {
algo: DigestAlgo::Blake3_256,
bytes: vec![9; 32],
}),
digest_state: DigestState::Present,
size_bytes: 3,
}),
now: Some(WireInstant {
unix_nanos: 5,
source: ClockSource::SignedFact,
trusted_until_unix_nanos: 9,
}),
}
}
#[test]
fn redaction_removes_every_field_that_was_not_granted() {
let bare = redact_context(ctx(), &[Capability::UsageRead]);
assert!(bare.principal.is_none());
assert!(bare.now.is_none());
let a = bare.artifact.unwrap();
assert!(a.digest.is_none());
// NOT NotKnown: the guest must be able to tell "you may not see it" from
// "the backend cannot produce one".
assert_eq!(a.digest_state, DigestState::NotGranted);
// The coordinates and the size are not behind a capability; a meter that
// could not see the size could not meter anything.
assert_eq!(a.size_bytes, 3);
}
#[test]
fn redaction_keeps_every_field_that_was_granted() {
let full = redact_context(
ctx(),
&[
Capability::PrincipalRead,
Capability::DigestRead,
Capability::ClockRead,
],
);
assert_eq!(full.principal.unwrap().ident, "sub-123");
assert_eq!(full.now.unwrap().source, ClockSource::SignedFact);
assert_eq!(full.artifact.unwrap().digest_state, DigestState::Present);
}
/// A meter with no manifest of its own, for the gate tests.
struct Bare(WireManifest2);
impl Meter for Bare {
fn manifest(&self) -> WireManifest2 {
self.0.clone()
}
fn meters(&self) -> Result<Vec<WireMeterDecl>, String> {
Ok(vec![])
}
fn read(&self, _h: &dyn MeterHost, _r: &str) -> Result<Vec<WireMeterReading>, String> {
Ok(vec![])
}
fn capacity(&self, _h: &dyn MeterHost) -> Result<WireCapacity, String> {
Ok(WireCapacity {
total_bytes: 0,
servable_bytes: 0,
used_bytes: 0,
source: ReadingValue::Refused {
reason: "bare".into(),
},
})
}
fn selftest(&self, _h: &dyn MeterHost) -> Result<WireSelftest, String> {
Ok(WireSelftest {
name: "bare".into(),
passed: true,
detail: String::new(),
})
}
fn evaluate(
&self,
_h: &dyn MeterHost,
_c: &WireCallContext,
) -> Result<WireVerdict, String> {
Ok(WireVerdict::Allow)
}
}
fn manifest(requires: Vec<Capability>) -> WireManifest2 {
WireManifest2 {
abi: AbiRange::new(2, 2),
kind: GuestKind::Meter,
name: "bare".into(),
requires,
handler: None,
meter: Some(MeterFacts {
accepts_facts: false,
}),
identity: None,
unknown_optional: Vec::new(),
}
}
#[test]
fn a_linked_meter_is_held_to_the_same_grant_a_loaded_one_is() {
let err = match LinkedMeter::new(
"bare",
Bare(manifest(vec![
Capability::UsageRead,
Capability::AuditWrite,
])),
Arc::new(EmptyStore),
Arc::new(CountingServices),
&[Capability::UsageRead],
) {
Ok(_) => panic!("an ungranted capability was accepted"),
Err(e) => e,
};
assert!(err.contains("audit.write"), "{err}");
}
#[test]
fn a_linked_meter_that_was_granted_what_it_asked_for_runs() {
let m = match LinkedMeter::new(
"bare",
Bare(manifest(vec![Capability::UsageRead])),
Arc::new(EmptyStore),
Arc::new(CountingServices),
&[Capability::UsageRead],
) {
Ok(m) => m,
Err(e) => panic!("{e}"),
};
assert_eq!(m.granted(), &[Capability::UsageRead]);
assert_eq!(m.evaluate(&ctx()).unwrap(), WireVerdict::Allow);
}
}