Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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);
    }
}