liminal-server 0.20.0

Standalone server for the liminal messaging bus
Documentation
//! The production bindings of the service seams (ACCESS-003 R1), built once
//! from `[auth.lys.services]`.
//!
//! - [`SeatStarter`](super::SeatStarter): [`LysSeatStarter`], a seat started
//!   by Lys's start-command route on the configured machine.
//! - [`KeySender`](super::KeySender): [`ChannelKeySender`], the keys
//!   published on `sessions.<id>.keys` through the server's own publish path.
//! - [`BudgetSource`](super::BudgetSource): [`LysBudgets`], budgets and stops
//!   read from Lys, carried whole.
//! - [`ServiceAction`](super::ServiceAction): [`PublishingActions`], alarms
//!   and launches published on their subjects, context readings reported to
//!   Lys through the [`ContextReporter`] the context watch shares.
//!
//! Every Lys request is made as this liminal, a machine Lys knows: the
//! bearer is this machine's own Lys pass (`[auth.lys.machine].pass_path`),
//! verified offline at start exactly as any presented pass is. Without
//! `[auth.lys.machine]` the services cannot ask Lys as anyone, and start is
//! refused by that name, never served with a guessed identity.

use super::actions::PublishingActions;
use super::budgets::LysBudgets;
use super::context_watch::ContextReporter;
use super::keys::{ChannelKeySender, ConnectionPublisher, ServicePublisher};
use super::lys_http::{HttpLys, LysHttp};
use super::seats::LysSeatStarter;
use super::{ServiceEffectError, Services};
use crate::auth_lys::LysPassConsumer;
use crate::config::types::LysConfig;
use crate::server::connection::LiminalConnectionServices;
use std::sync::Arc;

/// The four production effect seams, built once and lent as [`Services`].
pub struct ServiceBindings {
    seats: LysSeatStarter,
    keys: ChannelKeySender,
    budgets: LysBudgets,
    actions: PublishingActions,
    context: Arc<ContextReporter>,
    watch_context: bool,
}

impl std::fmt::Debug for ServiceBindings {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("ServiceBindings")
            .field("seats", &self.seats)
            .field("keys", &self.keys)
            .field("budgets", &self.budgets)
            .field("actions", &self.actions)
            .field("watch_context", &self.watch_context)
            .finish_non_exhaustive()
    }
}

impl ServiceBindings {
    /// Binds every seam: Lys asked through `http` with `bearer` (this
    /// machine's own pass), seats started on `machine`, messages published
    /// through `publisher`.
    #[must_use]
    pub fn new(
        http: &Arc<dyn LysHttp>,
        bearer: &str,
        machine: String,
        publisher: &Arc<dyn ServicePublisher>,
    ) -> Self {
        let context = Arc::new(ContextReporter::new(Arc::clone(http), bearer.to_owned()));
        Self {
            seats: LysSeatStarter::new(Arc::clone(http), bearer.to_owned(), machine),
            keys: ChannelKeySender::new(Arc::clone(publisher)),
            budgets: LysBudgets::new(Arc::clone(http), bearer.to_owned()),
            actions: PublishingActions::new(Arc::clone(publisher), Arc::clone(&context)),
            context,
            watch_context: false,
        }
    }

    /// Whether the operator asked for the context watch to run.
    #[must_use]
    pub const fn watches_context(&self) -> bool {
        self.watch_context
    }

    /// The seams, lent for one [`serve`](super::serve).
    #[must_use]
    pub fn services(&self) -> Services<'_> {
        Services {
            seats: &self.seats,
            keys: &self.keys,
            budgets: &self.budgets,
            actions: &self.actions,
        }
    }

    /// The reporter the context watch shares with the `context-watch` call.
    #[must_use]
    pub fn context_reporter(&self) -> Arc<ContextReporter> {
        Arc::clone(&self.context)
    }

    /// Builds the bindings `[auth.lys.services]` configures; `Ok(None)`
    /// without the section. Lys is asked over HTTP on `handle`'s runtime;
    /// messages are published through `services` as `liminal.<host>`.
    ///
    /// # Errors
    /// `[auth.lys.machine]` is absent (the services would have no identity to
    /// ask Lys as), this machine's pass or the issuer's key set cannot be
    /// read or does not verify, or the issuer is not an HTTP origin.
    pub fn configure(
        lys: &LysConfig,
        services: Arc<LiminalConnectionServices>,
        handle: tokio::runtime::Handle,
        now: u64,
    ) -> Result<Option<Self>, ServiceEffectError> {
        let Some(section) = lys.services.as_ref() else {
            return Ok(None);
        };
        let bearer = machine_bearer(lys, now)?;
        let http: Arc<dyn LysHttp> = Arc::new(HttpLys::new(&lys.issuer, handle)?);
        let publisher: Arc<dyn ServicePublisher> =
            Arc::new(ConnectionPublisher::new(services, section.host.clone()));
        let mut bindings = Self::new(&http, &bearer, section.machine.clone(), &publisher);
        bindings.watch_context = section.context_watch;
        Ok(Some(bindings))
    }
}

/// This machine's own Lys pass as the bearer the services ask Lys with.
///
/// It is verified offline for audience `liminal` at `now` against the
/// issuer's key set, so an expired or foreign pass refuses start by its
/// cause.
///
/// # Errors
/// No `[auth.lys.machine]`, or the pass or key set is unreadable or refused.
pub fn machine_bearer(lys: &LysConfig, now: u64) -> Result<String, ServiceEffectError> {
    let machine = lys.machine.as_ref().ok_or_else(|| {
        ServiceEffectError(
            "auth.lys.services needs auth.lys.machine: the services ask Lys as this machine"
                .to_owned(),
        )
    })?;
    let keys_json = std::fs::read_to_string(&lys.keys_path).map_err(|error| {
        ServiceEffectError(format!(
            "auth.lys.keys_path {}: {error}",
            lys.keys_path.display()
        ))
    })?;
    let adapter = crate::auth_lys::lys_pass_adapter::LysPassAdapter::from_keys_json(
        &keys_json,
        lys.issuer.clone(),
    )
    .map_err(|error| ServiceEffectError(format!("auth.lys key set refused: {error}")))?;
    let pass_bytes = std::fs::read(&machine.pass_path).map_err(|error| {
        ServiceEffectError(format!(
            "auth.lys.machine.pass_path {}: {error}",
            machine.pass_path.display()
        ))
    })?;
    adapter
        .verify_offline(&pass_bytes, crate::auth_lys::LIMINAL_PASS_AUDIENCE, now)
        .map_err(|error| ServiceEffectError(format!("auth.lys.machine pass refused: {error}")))?;
    let token = std::str::from_utf8(&pass_bytes)
        .map_err(|_| ServiceEffectError("auth.lys.machine pass is not UTF-8".to_owned()))?;
    Ok(token.trim().to_owned())
}