Skip to main content

liminal_server/services/
production.rs

1//! The production bindings of the service seams (ACCESS-003 R1), built once
2//! from `[auth.lys.services]`.
3//!
4//! - [`SeatStarter`](super::SeatStarter): [`LysSeatStarter`], a seat started
5//!   by Lys's start-command route on the configured machine.
6//! - [`KeySender`](super::KeySender): [`ChannelKeySender`], the keys
7//!   published on `sessions.<id>.keys` through the server's own publish path.
8//! - [`BudgetSource`](super::BudgetSource): [`LysBudgets`], budgets and stops
9//!   read from Lys, carried whole.
10//! - [`ServiceAction`](super::ServiceAction): [`PublishingActions`], alarms
11//!   and launches published on their subjects, context readings reported to
12//!   Lys through the [`ContextReporter`] the context watch shares.
13//!
14//! Every Lys request is made as this liminal, a machine Lys knows: the
15//! bearer is this machine's own Lys pass (`[auth.lys.machine].pass_path`),
16//! verified offline at start exactly as any presented pass is. Without
17//! `[auth.lys.machine]` the services cannot ask Lys as anyone, and start is
18//! refused by that name, never served with a guessed identity.
19
20use super::actions::PublishingActions;
21use super::budgets::LysBudgets;
22use super::context_watch::ContextReporter;
23use super::keys::{ChannelKeySender, ConnectionPublisher, ServicePublisher};
24use super::lys_http::{HttpLys, LysHttp};
25use super::seats::LysSeatStarter;
26use super::{ServiceEffectError, Services};
27use crate::auth_lys::LysPassConsumer;
28use crate::config::types::LysConfig;
29use crate::server::connection::LiminalConnectionServices;
30use std::sync::Arc;
31
32/// The four production effect seams, built once and lent as [`Services`].
33pub struct ServiceBindings {
34    seats: LysSeatStarter,
35    keys: ChannelKeySender,
36    budgets: LysBudgets,
37    actions: PublishingActions,
38    context: Arc<ContextReporter>,
39    watch_context: bool,
40}
41
42impl std::fmt::Debug for ServiceBindings {
43    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        formatter
45            .debug_struct("ServiceBindings")
46            .field("seats", &self.seats)
47            .field("keys", &self.keys)
48            .field("budgets", &self.budgets)
49            .field("actions", &self.actions)
50            .field("watch_context", &self.watch_context)
51            .finish_non_exhaustive()
52    }
53}
54
55impl ServiceBindings {
56    /// Binds every seam: Lys asked through `http` with `bearer` (this
57    /// machine's own pass), seats started on `machine`, messages published
58    /// through `publisher`.
59    #[must_use]
60    pub fn new(
61        http: &Arc<dyn LysHttp>,
62        bearer: &str,
63        machine: String,
64        publisher: &Arc<dyn ServicePublisher>,
65    ) -> Self {
66        let context = Arc::new(ContextReporter::new(Arc::clone(http), bearer.to_owned()));
67        Self {
68            seats: LysSeatStarter::new(Arc::clone(http), bearer.to_owned(), machine),
69            keys: ChannelKeySender::new(Arc::clone(publisher)),
70            budgets: LysBudgets::new(Arc::clone(http), bearer.to_owned()),
71            actions: PublishingActions::new(Arc::clone(publisher), Arc::clone(&context)),
72            context,
73            watch_context: false,
74        }
75    }
76
77    /// Whether the operator asked for the context watch to run.
78    #[must_use]
79    pub const fn watches_context(&self) -> bool {
80        self.watch_context
81    }
82
83    /// The seams, lent for one [`serve`](super::serve).
84    #[must_use]
85    pub fn services(&self) -> Services<'_> {
86        Services {
87            seats: &self.seats,
88            keys: &self.keys,
89            budgets: &self.budgets,
90            actions: &self.actions,
91        }
92    }
93
94    /// The reporter the context watch shares with the `context-watch` call.
95    #[must_use]
96    pub fn context_reporter(&self) -> Arc<ContextReporter> {
97        Arc::clone(&self.context)
98    }
99
100    /// Builds the bindings `[auth.lys.services]` configures; `Ok(None)`
101    /// without the section. Lys is asked over HTTP on `handle`'s runtime;
102    /// messages are published through `services` as `liminal.<host>`.
103    ///
104    /// # Errors
105    /// `[auth.lys.machine]` is absent (the services would have no identity to
106    /// ask Lys as), this machine's pass or the issuer's key set cannot be
107    /// read or does not verify, or the issuer is not an HTTP origin.
108    pub fn configure(
109        lys: &LysConfig,
110        services: Arc<LiminalConnectionServices>,
111        handle: tokio::runtime::Handle,
112        now: u64,
113    ) -> Result<Option<Self>, ServiceEffectError> {
114        let Some(section) = lys.services.as_ref() else {
115            return Ok(None);
116        };
117        let bearer = machine_bearer(lys, now)?;
118        let http: Arc<dyn LysHttp> = Arc::new(HttpLys::new(
119            lys.api_url.as_deref().unwrap_or(&lys.issuer),
120            handle,
121        )?);
122        let publisher: Arc<dyn ServicePublisher> =
123            Arc::new(ConnectionPublisher::new(services, section.host.clone()));
124        let mut bindings = Self::new(&http, &bearer, section.machine.clone(), &publisher);
125        bindings.watch_context = section.context_watch;
126        Ok(Some(bindings))
127    }
128}
129
130/// This machine's own Lys pass as the bearer the services ask Lys with.
131///
132/// It is verified offline for audience `liminal` at `now` against the
133/// issuer's key set, so an expired or foreign pass refuses start by its
134/// cause.
135///
136/// # Errors
137/// No `[auth.lys.machine]`, or the pass or key set is unreadable or refused.
138pub fn machine_bearer(lys: &LysConfig, now: u64) -> Result<String, ServiceEffectError> {
139    let machine = lys.machine.as_ref().ok_or_else(|| {
140        ServiceEffectError(
141            "auth.lys.services needs auth.lys.machine: the services ask Lys as this machine"
142                .to_owned(),
143        )
144    })?;
145    let keys_json = std::fs::read_to_string(&lys.keys_path).map_err(|error| {
146        ServiceEffectError(format!(
147            "auth.lys.keys_path {}: {error}",
148            lys.keys_path.display()
149        ))
150    })?;
151    let adapter = crate::auth_lys::lys_pass_adapter::LysPassAdapter::from_keys_json(
152        &keys_json,
153        lys.issuer.clone(),
154    )
155    .map_err(|error| ServiceEffectError(format!("auth.lys key set refused: {error}")))?;
156    let pass_bytes = std::fs::read(&machine.pass_path).map_err(|error| {
157        ServiceEffectError(format!(
158            "auth.lys.machine.pass_path {}: {error}",
159            machine.pass_path.display()
160        ))
161    })?;
162    adapter
163        .verify_offline(&pass_bytes, crate::auth_lys::LIMINAL_PASS_AUDIENCE, now)
164        .map_err(|error| ServiceEffectError(format!("auth.lys.machine pass refused: {error}")))?;
165    let token = std::str::from_utf8(&pass_bytes)
166        .map_err(|_| ServiceEffectError("auth.lys.machine pass is not UTF-8".to_owned()))?;
167    Ok(token.trim().to_owned())
168}