liminal-server 0.24.5

Standalone server for the liminal messaging bus
Documentation
//! SRV-038: whether the network has a root in its Lys, asked when the grants
//! are complete and said out loud.
//!
//! A server started against a Lys whose grant book holds nothing on its
//! network starts ready with no word: the stream is complete, merely empty,
//! and every grant-bound request is then refused. The question asked here is
//! Lys's own who-can (`POST /api/grants/who`): may any identity take `admin`
//! on `liminal.network:<network_id>`, one holder a page. It is asked when the
//! grant stream first reaches ready and at every reset it applies, never on a
//! timer and never per request.
//!
//! An absent root is one error naming the network and the Lys; a question Lys
//! refused or answered unreadably is one warning with a stable cause and no
//! response words; a held root is silent. `/health` carries the latest answer
//! as `lys_root`.

use std::sync::{Mutex, PoisonError};

/// Where the root question stands.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LysRoot {
    /// The grant stream has not yet been ready, so nothing was asked.
    NotAsked,
    /// One identity or more may take `admin` on the network.
    Held,
    /// No identity may: every grant-bound request will be refused.
    Absent,
    /// Lys refused the question or its answer could not be read.
    Unknown {
        /// The stable cause, never Lys's words.
        cause: &'static str,
    },
}

impl LysRoot {
    /// The stable cause of an unknown answer.
    #[must_use]
    pub const fn cause(&self) -> Option<&'static str> {
        match self {
            Self::Unknown { cause } => Some(cause),
            Self::NotAsked | Self::Held | Self::Absent => None,
        }
    }

    /// The value `/health` reports for this answer.
    #[must_use]
    pub const fn health(&self) -> &'static str {
        match self {
            Self::NotAsked => "not_asked",
            Self::Held => "held",
            Self::Absent => "absent",
            Self::Unknown { .. } => "unknown",
        }
    }
}

/// The root question for one network, and its latest answer.
pub struct RootPresence {
    network_id: String,
    api: reqwest::Url,
    credential: zeroize::Zeroizing<String>,
    client: reqwest::Client,
    handle: tokio::runtime::Handle,
    answer: Mutex<LysRoot>,
}

impl std::fmt::Debug for RootPresence {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("RootPresence")
            .field("network_id", &self.network_id)
            .field("answer", &self.answer())
            .finish_non_exhaustive()
    }
}

/// The who-can answer, read only for whether it names a holder.
#[derive(serde::Deserialize)]
struct WhoPage {
    holders: Vec<serde::de::IgnoredAny>,
}

impl RootPresence {
    /// The question for `network_id`, asked of the Lys API at `api_url` with
    /// this liminal's app `credential`, on `handle`'s runtime.
    ///
    /// # Errors
    /// `api_url` is not a URL, or the HTTP client cannot be built.
    pub fn new(
        network_id: &str,
        api_url: &str,
        credential: String,
        handle: tokio::runtime::Handle,
    ) -> Result<Self, lys_pass::Error> {
        let mut api: reqwest::Url = api_url
            .parse()
            .map_err(|_| lys_pass::Error::Invalid("the Lys API base is not a URL"))?;
        if !api.path().ends_with('/') {
            api.set_path(&format!("{}/", api.path()));
        }
        let client = reqwest::Client::builder()
            .no_proxy()
            .redirect(reqwest::redirect::Policy::none())
            .build()
            .map_err(|error| lys_pass::Error::Transport(Box::new(error)))?;
        Ok(Self {
            network_id: network_id.to_owned(),
            api,
            credential: zeroize::Zeroizing::new(credential),
            client,
            handle,
            answer: Mutex::new(LysRoot::NotAsked),
        })
    }

    /// The latest answer.
    #[must_use]
    pub fn answer(&self) -> LysRoot {
        self.answer
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .clone()
    }

    /// Asks once from a thread outside the runtime, records the answer and
    /// says it: an error when absent, a warning when unknown, nothing when held.
    pub fn ask(&self) {
        let handle = &self.handle;
        let answer =
            std::thread::scope(|scope| scope.spawn(|| handle.block_on(self.question())).join())
                .unwrap_or(LysRoot::Unknown {
                    cause: "lys_root_ask_panicked",
                });
        self.record(answer);
    }

    /// [`Self::ask`] from inside the runtime: the grant stream's reader asks
    /// here when a new ready fence opens.
    pub async fn ask_now(&self) {
        let answer = self.question().await;
        self.record(answer);
    }

    /// Stores an answer without saying it, for the health tests.
    #[cfg(test)]
    pub(crate) fn record_for_tests(&self, answer: LysRoot) {
        *self.answer.lock().unwrap_or_else(PoisonError::into_inner) = answer;
    }

    fn record(&self, answer: LysRoot) {
        match &answer {
            LysRoot::Absent => tracing::error!(
                target: "liminal::lys_root",
                network = %self.network_id,
                lys = %self.api,
                "the network has no root in its Lys: no identity may administer \
                 liminal.network:{}, so every grant-bound request is refused until a root \
                 is given there",
                self.network_id
            ),
            LysRoot::Unknown { cause } => tracing::warn!(
                target: "liminal::lys_root",
                network = %self.network_id,
                lys = %self.api,
                cause,
                "whether the network has a root in its Lys could not be read"
            ),
            LysRoot::Held | LysRoot::NotAsked => {}
        }
        *self.answer.lock().unwrap_or_else(PoisonError::into_inner) = answer;
    }

    async fn question(&self) -> LysRoot {
        let Ok(url) = self.api.join("grants/who") else {
            return LysRoot::Unknown {
                cause: "lys_root_route_invalid",
            };
        };
        let body = serde_json::json!({
            "route": "api",
            "resource": { "kind": "liminal.network", "id": self.network_id },
            "action": "admin",
            "page_size": 1,
            "after": null,
        });
        let page = async {
            let response = self
                .client
                .post(url)
                .bearer_auth(self.credential.as_str())
                .json(&body)
                .send()
                .await
                .map_err(|_| "lys_root_unreachable")?;
            if !response.status().is_success() {
                return Err("lys_root_refused");
            }
            response
                .json::<WhoPage>()
                .await
                .map_err(|_| "lys_root_unreadable")
        };
        match page.await {
            Ok(page) if page.holders.is_empty() => LysRoot::Absent,
            Ok(_) => LysRoot::Held,
            Err(cause) => LysRoot::Unknown { cause },
        }
    }
}