liminal-server 0.21.1

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 subject's placed prefix 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;

/// 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 subject's placed prefix.
    Export,
    /// Held by the importing machine on the subject's placed prefix.
    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>,
}

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,
        }
    }

    /// 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()
    }
}

/// A machine's pass answering, offline, whether it holds a link grant on a
/// bound subject. The real answer comes from the pass's rights by placed
/// prefix; a fixture answers from a table.
pub trait LinkRights: Send + Sync + std::fmt::Debug {
    /// Whether this machine's pass holds `action` reaching `subject` 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 `subject`: 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::new(
            self.network_id.clone(),
            subject.to_owned(),
            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 })
    }
}