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