liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! A link is a machine's grant (ACCESS-002 R1).
//!
//! A link is a resource placed under the network, between an exporting and
//! an importing machine. Opening it is deliberate and asks Lys through the
//! live seam. Nothing crosses without a grant on both ends: the exporting
//! machine's pass must hold `link_export` on the link resource and
//! the importing machine's pass `link_import`, both decided offline from the
//! passes, and a message refused on either end is refused by name saying
//! which end. The handshake that keys the link lives in
//! [`crate::link_handshake`].

use crate::auth_lys::{
    AccessAction, AccessDecisionError, AccessResource, AdmittedLysPass, DeliberateDecision,
    GrantReceipt, LysDeliberateConsumer, ResourceKind, SubjectBinding, SubjectBindingError,
    decide_deliberate,
};
use crate::link_handshake::{LocalMachine, MachineIdentity};
use std::collections::BTreeMap;

#[path = "link_route_call.rs"]
pub mod calls;
#[path = "link_certificate.rs"]
pub mod certificate;
#[path = "link_federated.rs"]
pub mod federated;
mod life;
pub use life::LinkLifeCounters;
pub(crate) use life::{CloseLinks, LinkLife, LinkRegistry};

/// The carrier event that ended a keyed link.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum LinkEnd {
    /// The remote carrier disappeared.
    #[error("peer_gone")]
    PeerGone,
    /// The admitted pass lost its grant.
    #[error("revoked")]
    Revoked,
    /// The link was ended by a named refusal.
    #[error("refused: {0}")]
    Refused(String),
    /// Another link took this link's place.
    #[error("replaced")]
    Replaced,
    /// The owner shut the link down.
    #[error("shutdown")]
    Shutdown,
}

/// The two grants a crossing needs, one per end.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinkAction {
    /// Held by the exporting machine on the link resource.
    Export,
    /// Held by the importing machine on the link resource.
    Import,
}

impl LinkAction {
    /// The schema action this end's grant names.
    #[must_use]
    pub const fn access_action(self) -> AccessAction {
        match self {
            Self::Export => AccessAction::LinkExport,
            Self::Import => AccessAction::LinkImport,
        }
    }

    /// The other end of the same link.
    #[must_use]
    pub const fn other(self) -> Self {
        match self {
            Self::Export => Self::Import,
            Self::Import => Self::Export,
        }
    }
}

impl From<liminal::protocol::LinkEnd> for LinkAction {
    fn from(end: liminal::protocol::LinkEnd) -> Self {
        match end {
            liminal::protocol::LinkEnd::Export => Self::Export,
            liminal::protocol::LinkEnd::Import => Self::Import,
        }
    }
}

/// This liminal as a machine Lys knows (ACCESS-002 R1): the identity its
/// links are keyed from, its own pass's link rights, and the machine keys
/// Lys recorded for the holders that may open links to it.
///
/// The recorded keys are the operator's mirror of Lys's directory
/// (`[auth.lys.machine] machines`) until liminal reads the directory itself;
/// a holder with no recorded key cannot open a link, and an offered key that
/// is not the recorded one is refused by name.
#[derive(Debug)]
pub struct LinkHost {
    local: LocalMachine,
    rights: Box<dyn LinkRights>,
    recorded: BTreeMap<String, MachineIdentity>,
    peers: Option<crate::config::peers::PeersConfig>,
    service_key: Option<[u8; 32]>,
    certificate_standing: Option<std::sync::Arc<certificate::standing::Standing>>,
    far: Option<std::sync::Arc<federated::FarTopology>>,
    executor: std::sync::OnceLock<std::sync::Weak<dyn calls::CallExecutor>>,
}

impl LinkHost {
    /// This machine, its own link rights, and the recorded keys by holder.
    #[must_use]
    pub const fn new(
        local: LocalMachine,
        rights: Box<dyn LinkRights>,
        recorded: BTreeMap<String, MachineIdentity>,
    ) -> Self {
        Self {
            local,
            rights,
            recorded,
            peers: None,
            service_key: None,
            certificate_standing: None,
            far: None,
            executor: std::sync::OnceLock::new(),
        }
    }

    /// The identity this machine's links are keyed from.
    #[must_use]
    pub const fn local(&self) -> &LocalMachine {
        &self.local
    }

    /// This machine's own pass answering its end of every crossing.
    #[must_use]
    pub fn rights(&self) -> &dyn LinkRights {
        self.rights.as_ref()
    }

    /// The machine key Lys recorded for `holder`, if one is recorded.
    #[must_use]
    pub fn recorded_machine(&self, holder: &str) -> Option<MachineIdentity> {
        self.recorded.get(holder).copied()
    }

    /// Installs the service-key trust anchor and explicit device-link bounds.
    ///
    /// # Errors
    /// A configured allocation bound or subject prefix is invalid.
    pub fn with_peer_security(
        mut self,
        peers: crate::config::peers::PeersConfig,
        service_key: [u8; 32],
    ) -> Result<Self, &'static str> {
        peers.validate()?;
        let verifying = ed25519_dalek::VerifyingKey::from_bytes(&service_key)
            .map_err(|_| "certificate_service_key_refused")?;
        if verifying.is_weak() {
            return Err("certificate_service_key_refused");
        }
        self.far = Some(std::sync::Arc::new(federated::FarTopology::new(
            federated::ProjectionLimits {
                bytes: peers.max_topology_bytes,
                stations: peers.max_stations,
                peers: peers.max_peers,
            },
        )));
        self.peers = Some(peers);
        self.service_key = Some(service_key);
        Ok(self)
    }
    /// Configured bounds; absence preserves the original stream-only path.
    #[must_use]
    pub const fn peers(&self) -> Option<&crate::config::peers::PeersConfig> {
        self.peers.as_ref()
    }
    /// The generation-bound far index shared by all views.
    #[must_use]
    pub const fn far(&self) -> Option<&std::sync::Arc<federated::FarTopology>> {
        self.far.as_ref()
    }
    /// The service key fetched from the issuer API, never the pass JWKS.
    #[must_use]
    pub const fn service_key(&self) -> Option<&[u8; 32]> {
        self.service_key.as_ref()
    }
    /// The issuer's existing certified-agent route supplies current standing.
    #[must_use]
    pub const fn standing(&self) -> Option<&std::sync::Arc<certificate::standing::Standing>> {
        self.certificate_standing.as_ref()
    }
    /// Installs the issuer's standing verifier built from this host's pass adapter.
    #[must_use]
    pub fn with_standing(
        mut self,
        standing: std::sync::Arc<certificate::standing::Standing>,
    ) -> Self {
        self.certificate_standing = Some(standing);
        self
    }
    /// Installs the full far connection executor once, after its supervisor exists.
    ///
    /// # Errors
    /// A second executor would split the ownership of pending calls.
    pub fn install_call_executor(
        &self,
        executor: &std::sync::Arc<dyn calls::CallExecutor>,
    ) -> Result<(), calls::CallError> {
        self.executor
            .set(std::sync::Arc::downgrade(executor))
            .map_err(|_| calls::CallError::Unavailable)
    }
    /// The full far admission route, when its startup hook has installed it.
    #[must_use]
    pub fn call_executor(&self) -> Option<std::sync::Arc<dyn calls::CallExecutor>> {
        self.executor.get().and_then(std::sync::Weak::upgrade)
    }
}

/// A machine's pass answering, offline, whether it holds a link grant on a
/// bound link. The real answer comes from the verified pass; a fixture
/// answers from a table.
pub trait LinkRights: Send + Sync + std::fmt::Debug {
    /// Whether this machine's pass holds `action` on the bound link at `now`.
    ///
    /// # Errors
    /// The one refusal shape with its cause.
    fn holds(
        &self,
        action: LinkAction,
        subject: &SubjectBinding,
        now: u64,
    ) -> Result<GrantReceipt, AccessDecisionError>;
}

/// A link between two machines under one network.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Link {
    network_id: String,
    id: String,
    exporter: MachineIdentity,
    importer: MachineIdentity,
}

/// Why a link was not described.
#[derive(Debug, PartialEq, Eq)]
pub enum LinkError {
    /// An empty or whitespace-bearing identifier.
    Identifier(&'static str),
    /// The same machine on both ends.
    SameMachine,
}

impl std::fmt::Display for LinkError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Identifier(member) => write!(formatter, "link {member} is not an identifier"),
            Self::SameMachine => formatter.write_str("a link joins two different machines"),
        }
    }
}

impl std::error::Error for LinkError {}

/// Why a message did not cross, naming the end that refused it.
#[derive(Debug, PartialEq, Eq)]
pub enum LinkRefusal {
    /// The subject could not be bound (empty, whitespace, or a pattern).
    Binding(SubjectBindingError),
    /// The exporting machine's pass holds no reaching `link_export`.
    Export(AccessDecisionError),
    /// The importing machine's pass holds no reaching `link_import`.
    Import(AccessDecisionError),
}

impl std::fmt::Display for LinkRefusal {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Binding(SubjectBindingError(member)) => {
                write!(formatter, "crossing refused: subject {member}")
            }
            Self::Export(error) => write!(formatter, "crossing refused at export: {error}"),
            Self::Import(error) => write!(formatter, "crossing refused at import: {error}"),
        }
    }
}

impl std::error::Error for LinkRefusal {}

/// The two receipts a crossing was decided on.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Crossing {
    /// The exporting machine's grant.
    pub export: GrantReceipt,
    /// The importing machine's grant.
    pub import: GrantReceipt,
}

impl Link {
    /// Describes a link under `network_id` between two different machines.
    ///
    /// # Errors
    /// An identifier that is empty or carries whitespace, or one machine on
    /// both ends.
    pub fn new(
        network_id: String,
        id: String,
        exporter: MachineIdentity,
        importer: MachineIdentity,
    ) -> Result<Self, LinkError> {
        for (member, value) in [("network", &network_id), ("id", &id)] {
            if value.is_empty() || value.chars().any(char::is_whitespace) {
                return Err(LinkError::Identifier(member));
            }
        }
        if exporter == importer {
            return Err(LinkError::SameMachine);
        }
        Ok(Self {
            network_id,
            id,
            exporter,
            importer,
        })
    }

    /// The network the link is placed under.
    #[must_use]
    pub fn network_id(&self) -> &str {
        &self.network_id
    }

    /// The link as a placed resource of kind `link`.
    #[must_use]
    pub fn resource(&self) -> AccessResource {
        AccessResource {
            kind: ResourceKind::Link,
            id: self.id.clone(),
        }
    }

    /// The machine messages leave.
    #[must_use]
    pub const fn exporter(&self) -> &MachineIdentity {
        &self.exporter
    }

    /// The machine messages arrive at.
    #[must_use]
    pub const fn importer(&self) -> &MachineIdentity {
        &self.importer
    }

    /// Asks Lys, live, whether this end may open the link: the exporting
    /// end asks for `link_export` on the link, the importing end for
    /// `link_import`. Never decided from the pass alone; the seam's absence is
    /// the `deliberate_unavailable` refusal.
    ///
    /// # Errors
    /// The one refusal shape with its cause.
    pub fn open<C: LysDeliberateConsumer>(
        &self,
        end: LinkAction,
        consumer: &C,
        pass: &AdmittedLysPass<C::Pass>,
        now: u64,
    ) -> Result<DeliberateDecision, AccessDecisionError> {
        decide_deliberate(consumer, pass, &self.resource(), end.access_action(), now)
    }

    /// Decides one message crossing on this link resource: the exporter's pass first,
    /// then the importer's, both offline; either refusal names its end.
    ///
    /// # Errors
    /// The subject could not be bound, or an end holds no reaching grant.
    pub fn crossing(
        &self,
        exporter: &dyn LinkRights,
        importer: &dyn LinkRights,
        subject: &str,
        now: u64,
    ) -> Result<Crossing, LinkRefusal> {
        let bound =
            SubjectBinding::link(self.network_id.clone(), self.id.clone(), subject.to_owned())
                .map_err(LinkRefusal::Binding)?;
        let export = exporter
            .holds(LinkAction::Export, &bound, now)
            .map_err(LinkRefusal::Export)?;
        let import = importer
            .holds(LinkAction::Import, &bound, now)
            .map_err(LinkRefusal::Import)?;
        Ok(Crossing { export, import })
    }
}