liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! A person's outright agent act on the administration plane, taken at once.

use serde_json::json;

use super::{
    AdminDrafts, AdminResponse, AtomicDraftStore, DraftError, LysPassConsumer, Target, refused,
};

/// A verified person's pass and the client that asks Lys about it, owning everything the
/// question needs, so the asking future borrows no draft store across its await.
pub struct OutrightAsk {
    verified: crate::auth_lys::lys_pass_adapter::LysPass,
    client: lys_pass::Client,
}

impl<S: AtomicDraftStore> AdminDrafts<S> {
    /// Admits a person's pass for an outright act: Lys answers "act at once if the right is
    /// outright", so a held right keeps its draft and is refused by [`OutrightAsk::check`].
    ///
    /// # Errors
    /// The Lys client could not be built.
    pub fn outright(
        &self,
        authorization: Option<&str>,
        at_ms: u64,
    ) -> Result<Result<OutrightAsk, AdminResponse>, DraftError> {
        let Some(pass) = authorization.and_then(|header| header.strip_prefix("Bearer ")) else {
            return Ok(Err(refused(401, "agent_create_pass_missing")));
        };
        if pass.is_empty() || pass.len() > self.limits.credential_bytes.get() {
            return Ok(Err(refused(401, "agent_create_pass_invalid")));
        }
        let Ok(verified) = self
            .adapter
            .verify_offline(pass.as_bytes(), "liminal", at_ms / 1000)
        else {
            return Ok(Err(refused(401, "agent_create_pass_invalid")));
        };
        if verified.verified().claims().holder.kind != "person" {
            return Ok(Err(refused(403, "agent_create_person_required")));
        }
        let http = reqwest::Client::builder()
            .no_proxy()
            .redirect(reqwest::redirect::Policy::none())
            .build()
            .map_err(|_| DraftError::Lys("transport_failed".to_owned()))?;
        let issuer = crate::auth_lys::live::lys_origin(
            self.ask.as_deref().unwrap_or_else(|| self.adapter.issuer()),
        )
        .map_err(|_| DraftError::Contract)?;
        let client = lys_pass::Client::new(http, issuer)
            .map_err(|error| DraftError::Lys(error.name().to_owned()))?;
        Ok(Ok(OutrightAsk { verified, client }))
    }
}

impl OutrightAsk {
    /// Asks Lys whether the pass's holder holds `target` outright; answers the pass and its
    /// effective grant, or the refusal.
    ///
    /// # Errors
    /// Lys could not be asked.
    pub async fn check(
        self,
        target: &Target,
        connector_pass: &str,
    ) -> Result<
        Result<(crate::auth_lys::lys_pass_adapter::LysPass, String), AdminResponse>,
        DraftError,
    > {
        let holder = self.verified.verified().claims().holder.id.clone();
        let answer = self
            .client
            .check_batch(connector_pass, &holder, std::slice::from_ref(target), None)
            .await
            .map_err(|error| DraftError::Lys(error.name().to_owned()))?;
        let Some(result) = answer.results.first() else {
            return Err(DraftError::Contract);
        };
        match (&result.grant, result.allowed, result.mode) {
            (Some(grant), true, Some(lys_pass::Mode::Outright)) => {
                Ok(Ok((self.verified, grant.clone())))
            }
            _ => Ok(Err(AdminResponse {
                status: 403,
                body: json!({"refusal": result.refusal.clone().unwrap_or_else(|| "agent_create_outright_required".to_owned())}),
            })),
        }
    }
}