//! Client for `swarm-authelia-bridge` — the only writer of the swarm's //! authelia users database (see that crate's README for why this daemon //! cannot write it directly). //! //! Authenticated with THIS daemon's own queue OIDC identity //! (`SWARM_CONTROLLER_OIDC_*`, the same one `swarm-queue-client` mints for //! the queue connection) — "one identity per principal" means a second //! op that needs to prove who this process is reuses the identity it //! already has rather than provisioning a new one. A fresh token is //! minted per call for the same reason the queue client mints one per //! connection attempt: no window in which this process holds a token //! that outlives its intended use. //! //! `None` when this deployment did not wire a bridge up — the bridge only //! exists on hosts that also run `swarm-authelia`, so a controller split //! from it simply has no identity-creation capability yet //! (`SwarmNodeKind::CreateIdentity` fails such a job explicitly rather //! than this module papering over the gap). use anyhow::{Context, Result}; use swarm_authelia_bridge_sock::{BridgeRequest, BridgeResponse}; /// A configured connection to `swarm-authelia-bridge`. #[derive(Clone)] pub struct AuthBridge { http: reqwest::Client, base_url: String, queue_cfg: swarm_queue_client::QueueConfig, } impl AuthBridge { /// Read `SWARM_CONTROLLER_AUTH_BRIDGE_URL`; `Ok(None)` when unset. /// /// The queue identity (`SWARM_CONTROLLER_OIDC_*`) is not optional once /// the bridge URL is set: the nix module sets `queueEnv` unconditionally /// for every controller (the queue is required, not just co-located /// service), so a bridge URL with no queue identity to authenticate /// with is a deployment bug, not an absent-feature case — hence the /// hard error rather than a second `None`. pub fn from_env() -> Result> { let Ok(base_url) = std::env::var("SWARM_CONTROLLER_AUTH_BRIDGE_URL") else { return Ok(None); }; let queue_cfg = swarm_queue_client::QueueConfig::from_env("SWARM_CONTROLLER") .context("reading the queue OIDC identity the auth bridge authenticates with")? .ok_or_else(|| { anyhow::anyhow!( "SWARM_CONTROLLER_AUTH_BRIDGE_URL is set but SWARM_CONTROLLER_OIDC_* is \ not — the bridge is authenticated with this daemon's queue identity, so \ that identity must exist first" ) })?; Ok(Some(Self { http: reqwest::Client::new(), base_url, queue_cfg, })) } /// Idempotently ensure `name` exists as an authelia subject. pub async fn ensure_agent_identity(&self, name: &str) -> Result { // `mint_token_for` builds its own HTTP client (trusting the queue's // configured CA, if any) — deliberately not `self.http`, which is // the bridge's own client and has nothing to do with authelia's // token endpoint's trust anchors. let token = swarm_queue_client::mint_token_for(&self.queue_cfg) .await .context("minting a bearer token for swarm-authelia-bridge")?; let response = self .http .post(format!("{}/requests", self.base_url)) .bearer_auth(token) .json(&BridgeRequest::EnsureAgentIdentity { name: name.to_owned(), }) .send() .await .context("calling swarm-authelia-bridge")?; let status = response.status(); let body = response.text().await.unwrap_or_default(); if !status.is_success() { anyhow::bail!("swarm-authelia-bridge refused the request ({status}): {body}"); } serde_json::from_str(&body).context("parsing swarm-authelia-bridge's response") } } #[cfg(test)] mod tests { use super::*; /// The common case: no bridge wired up for this deployment. #[test] fn absent_url_is_not_an_error() { assert!(std::env::var("SWARM_CONTROLLER_AUTH_BRIDGE_URL").is_err()); assert!( AuthBridge::from_env() .expect("absent is not an error") .is_none() ); } }