Skip to main content

liminal_server/server/connection/supervisor/
builder.rs

1use super::{
2    Arc, AuthConfig, ConnectionNotifier, ConnectionRuntime, ConnectionServices,
3    ConnectionSupervisor, LimitsConfig, PassVerifier, ServerError, SupervisorInner,
4};
5
6/// The configured `[auth]` section as the supervisor carries it: the bearer
7/// token as opaque bytes for the handshake's constant-time comparison, and the
8/// registry pass verifier built from `auth.pass` when one is configured. The
9/// single derivation behind [`ConnectionSupervisor::from_config`] and
10/// [`ConnectionSupervisorBuilder::auth`], so the two cannot drift.
11pub(super) fn configured_authentication(
12    auth: &AuthConfig,
13) -> Result<(Vec<u8>, Option<PassVerifier>), ServerError> {
14    let pass_verifier = PassVerifier::from_auth(auth)?;
15    Ok((auth.token.clone().into_bytes(), pass_verifier))
16}
17
18/// Composes a [`ConnectionSupervisor`] over caller-owned services.
19///
20/// Any of the optional parts — `[auth]` (bearer + pass verifier), a connection
21/// notifier, operational limits — can be set together. Minted by
22/// [`ConnectionSupervisor::builder`].
23pub struct ConnectionSupervisorBuilder {
24    pub(super) services: Arc<dyn ConnectionServices>,
25    pub(super) notifier: Option<Arc<dyn ConnectionNotifier>>,
26    pub(super) auth_token: Option<Vec<u8>>,
27    pub(super) pass_verifier: Option<PassVerifier>,
28    pub(super) lys: Option<LysInstall>,
29    pub(super) limits: LimitsConfig,
30}
31
32// The bearer token prints as its length, never its bytes (PROTO-007 R2).
33impl std::fmt::Debug for ConnectionSupervisorBuilder {
34    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        formatter
36            .debug_struct("ConnectionSupervisorBuilder")
37            .field("services", &self.services)
38            .field("notifier", &self.notifier)
39            .field("auth_token_len", &self.auth_token.as_ref().map(Vec::len))
40            .field("pass_verifier", &self.pass_verifier)
41            .field("lys", &self.lys)
42            .field("limits", &self.limits)
43            .finish()
44    }
45}
46
47/// What `[auth.lys]` installs on the connection runtime: the admitter with
48/// the network id its subjects are placed under (ACCESS-001 R2), the
49/// machine's link host (ACCESS-002 R1), the service bindings (ACCESS-003 R1)
50/// and the channel membership gate (ACCESS-006 R1).
51#[derive(Debug)]
52pub struct LysInstall {
53    /// Admits a Lys pass at `Connect` and decides its routes.
54    pub(crate) admitter: Arc<dyn crate::auth_lys::session::LysAdmitter>,
55    /// The network every subject is placed under.
56    pub(crate) network_id: String,
57    /// This machine's link host, `[auth.lys.machine]`.
58    pub(crate) link_host: Option<Arc<crate::link::LinkHost>>,
59    /// The service effect seams, `[auth.lys.services]`.
60    pub(crate) services: Option<Arc<crate::services::production::ServiceBindings>>,
61    /// The channel membership gate `[auth.lys.membership]` configures, over
62    /// the same issuer.
63    pub(crate) membership: Option<Arc<crate::auth_membership::MembershipGate>>,
64}
65
66/// The tokio runtime Lys is asked on, live deliberate decisions and the
67/// service bindings' HTTP alike, owned by the server for its life; `None`
68/// without `[auth.lys]`. One worker: every ask is one short request made
69/// from a connection's slice through `Handle::block_on` on a scoped thread,
70/// never a runtime nested in the connection scheduler.
71///
72/// # Errors
73/// The runtime could not start, by name.
74pub fn lys_ask_runtime(
75    auth: Option<&AuthConfig>,
76) -> Result<Option<tokio::runtime::Runtime>, ServerError> {
77    if auth.is_none_or(|auth| auth.lys.is_none()) {
78        return Ok(None);
79    }
80    tokio::runtime::Builder::new_multi_thread()
81        .worker_threads(1)
82        .thread_name("liminal-lys-ask")
83        .enable_all()
84        .build()
85        .map(Some)
86        .map_err(|error| ServerError::ConfigValidation {
87            message: format!("the Lys ask runtime could not start: {error}"),
88        })
89}
90
91/// Builds the Lys admitter `[auth.lys]` configures, reading the issuer's key
92/// set once; `None` without the section. `ask_handle` is the runtime live
93/// asks run on ([`lys_ask_runtime`], or an embedder's own); `auth.lys.live`
94/// without one is refused at start, by name.
95///
96/// # Errors
97/// A key set, pass or identity that cannot be read or is refused, by name.
98pub fn configured_lys(
99    auth: &AuthConfig,
100    ask_handle: Option<tokio::runtime::Handle>,
101) -> Result<Option<LysInstall>, ServerError> {
102    Ok(lys_install(auth, ask_handle, false)?.map(|(install, _)| install))
103}
104
105/// [`configured_lys`] for a server that binds before it asks: the machine
106/// pass's first issue, when nothing built before the bind reads the pass,
107/// is returned to be begun once the listener binds (SRV-041 R4).
108///
109/// # Errors
110/// As [`configured_lys`].
111pub fn configured_lys_after_bind(
112    auth: &AuthConfig,
113    ask_handle: Option<tokio::runtime::Handle>,
114) -> Result<(Option<LysInstall>, Option<crate::link_pass::StartIssue>), ServerError> {
115    Ok(lys_install(auth, ask_handle, true)?
116        .map_or((None, None), |(install, issue)| (Some(install), issue)))
117}
118
119fn lys_install(
120    auth: &AuthConfig,
121    ask_handle: Option<tokio::runtime::Handle>,
122    after_bind: bool,
123) -> Result<Option<(LysInstall, Option<crate::link_pass::StartIssue>)>, ServerError> {
124    let Some(lys) = auth.lys.as_ref() else {
125        return Ok(None);
126    };
127    let keys_json =
128        std::fs::read_to_string(&lys.keys_path).map_err(|error| ServerError::ConfigValidation {
129            message: format!("auth.lys.keys_path {}: {error}", lys.keys_path.display()),
130        })?;
131    let mut adapter = crate::auth_lys::lys_pass_adapter::LysPassAdapter::from_keys_json(
132        &keys_json,
133        lys.issuer.clone(),
134    )
135    .map_err(|error| ServerError::ConfigValidation {
136        message: format!("auth.lys key set refused: {error}"),
137    })?
138    .with_decisions(lys.decisions);
139    if lys.live {
140        // Deliberate decisions ask the issuer live, with the holder's pass,
141        // on the server's runtime; the ask never nests a runtime.
142        let Some(handle) = ask_handle.as_ref() else {
143            return Err(ServerError::ConfigValidation {
144                message:
145                    "auth.lys.live needs the server's Lys ask runtime at start; none was given"
146                        .to_owned(),
147            });
148        };
149        let mut live = crate::auth_lys::live::HttpLysAsk::new(
150            lys.api_url.as_deref().unwrap_or(&lys.issuer),
151            handle.clone(),
152        )
153        .map_err(|error| ServerError::ConfigValidation {
154            message: format!("auth.lys.issuer refused for live asks: {error}"),
155        })?;
156        // Lys answers its batch check for an administrator or an app, never
157        // a seat's pass: with an app credential configured the question is
158        // asked as this liminal, about the holder.
159        if let Some(path) = lys.app_credential_path.as_ref() {
160            let credential =
161                std::fs::read_to_string(path).map_err(|error| ServerError::ConfigValidation {
162                    message: format!(
163                        "auth.lys.app_credential_path '{}' cannot be read: {}",
164                        path.display(),
165                        error.kind()
166                    ),
167                })?;
168            live = live.asking_as(credential.trim().to_owned());
169        }
170        adapter = adapter.with_live(Arc::new(live));
171    }
172    let adapter = Arc::new(adapter);
173    let now = std::time::SystemTime::now()
174        .duration_since(std::time::UNIX_EPOCH)
175        .map_err(|_| ServerError::ConfigValidation {
176            message: "machine pass clock is before the Unix epoch".to_owned(),
177        })?
178        .as_secs();
179    let machine_pass = crate::link_pass::configured(lys, adapter, ask_handle, now, after_bind)?;
180    let membership = lys
181        .membership
182        .as_ref()
183        .map(|membership| crate::auth_membership::configured_gate(membership, &lys.issuer))
184        .transpose()?;
185    Ok(Some((
186        LysInstall {
187            admitter: machine_pass.admitter,
188            network_id: lys.network_id.clone(),
189            link_host: machine_pass.host,
190            services: None,
191            membership,
192        },
193        machine_pass.issue,
194    )))
195}
196
197impl ConnectionSupervisorBuilder {
198    /// Installs the configured `[auth]` section: the bearer token, and the
199    /// registry pass verifier when `auth.pass` is present — derived exactly as
200    /// [`ConnectionSupervisor::from_config`] derives them.
201    ///
202    /// # Errors
203    /// Returns [`ServerError::ConfigValidation`] when `auth.pass` names a key
204    /// that is not exactly one valid Ed25519 verifying key as 64 hexadecimal
205    /// digits — the same error, with the same text, that `from_config` returns
206    /// for that configuration.
207    pub fn auth(mut self, auth: &AuthConfig) -> Result<Self, ServerError> {
208        let (auth_token, pass_verifier) = configured_authentication(auth)?;
209        self.auth_token = Some(auth_token);
210        self.pass_verifier = pass_verifier;
211        self.lys = configured_lys(auth, tokio::runtime::Handle::try_current().ok())?;
212        Ok(self)
213    }
214
215    /// Test seam: installs `lys` exactly as `[auth.lys]` would, with no
216    /// configuration file and no issuer key behind it.
217    #[cfg(test)]
218    #[must_use]
219    pub(in crate::server::connection) fn lys_for_tests(mut self, lys: LysInstall) -> Self {
220        self.lys = Some(lys);
221        self
222    }
223
224    /// Installs the connection-keyed notifier, invoked on worker registration
225    /// lifecycle and on pass-stamped attach/detach.
226    #[must_use]
227    pub fn notifier(mut self, notifier: Arc<dyn ConnectionNotifier>) -> Self {
228        self.notifier = Some(notifier);
229        self
230    }
231
232    /// Installs the operational limits (§5); unset, the signed defaults apply.
233    #[must_use]
234    pub const fn limits(mut self, limits: LimitsConfig) -> Self {
235        self.limits = limits;
236        self
237    }
238
239    /// Starts the supervisor.
240    ///
241    /// # Errors
242    /// Returns [`ServerError`] when incarnation startup or scheduler startup
243    /// fails.
244    pub fn build(self) -> Result<ConnectionSupervisor, ServerError> {
245        SupervisorInner::new(
246            self.services,
247            self.notifier,
248            self.auth_token,
249            self.pass_verifier,
250            self.lys,
251            self.limits,
252            None,
253        )
254        .map(|inner| ConnectionSupervisor {
255            inner: Arc::new(inner),
256        })
257    }
258}
259
260/// Installs the `[auth.lys]` parts on the runtime, each once: the admitter,
261/// the link host, the membership gate and the service bindings.
262pub(super) fn install_lys(runtime: &ConnectionRuntime, lys: LysInstall) -> Result<(), ServerError> {
263    let LysInstall {
264        admitter,
265        network_id,
266        link_host,
267        services: bindings,
268        membership,
269    } = lys;
270    runtime
271        .install_lys_admitter(admitter, network_id)
272        .map_err(|_| ServerError::ConfigValidation {
273            message: "auth.lys admitter was already installed".to_owned(),
274        })?;
275    if let Some(host) = link_host {
276        runtime
277            .install_link_host(host)
278            .map_err(|_| ServerError::ConfigValidation {
279                message: "auth.lys.machine was already installed".to_owned(),
280            })?;
281    }
282    if let Some(gate) = membership {
283        runtime
284            .install_membership_gate(gate)
285            .map_err(|_| ServerError::ConfigValidation {
286                message: "auth.lys.membership was already installed".to_owned(),
287            })?;
288    }
289    if let Some(bindings) = bindings {
290        runtime
291            .install_service_bindings(bindings)
292            .map_err(|_| ServerError::ConfigValidation {
293                message: "auth.lys.services was already installed".to_owned(),
294            })?;
295    }
296    Ok(())
297}