Skip to main content

liminal_server/http/
admin_outright.rs

1//! A person's outright agent act on the administration plane, taken at once.
2
3use serde_json::json;
4
5use super::{
6    AdminDrafts, AdminResponse, AtomicDraftStore, DraftError, LysPassConsumer, Target, refused,
7};
8
9/// A verified person's pass and the client that asks Lys about it, owning everything the
10/// question needs, so the asking future borrows no draft store across its await.
11pub struct OutrightAsk {
12    verified: crate::auth_lys::lys_pass_adapter::LysPass,
13    client: lys_pass::Client,
14}
15
16impl<S: AtomicDraftStore> AdminDrafts<S> {
17    /// Admits a person's pass for an outright act: Lys answers "act at once if the right is
18    /// outright", so a held right keeps its draft and is refused by [`OutrightAsk::check`].
19    ///
20    /// # Errors
21    /// The Lys client could not be built.
22    pub fn outright(
23        &self,
24        authorization: Option<&str>,
25        at_ms: u64,
26    ) -> Result<Result<OutrightAsk, AdminResponse>, DraftError> {
27        let Some(pass) = authorization.and_then(|header| header.strip_prefix("Bearer ")) else {
28            return Ok(Err(refused(401, "agent_create_pass_missing")));
29        };
30        if pass.is_empty() || pass.len() > self.limits.credential_bytes.get() {
31            return Ok(Err(refused(401, "agent_create_pass_invalid")));
32        }
33        let Ok(verified) = self
34            .adapter
35            .verify_offline(pass.as_bytes(), "liminal", at_ms / 1000)
36        else {
37            return Ok(Err(refused(401, "agent_create_pass_invalid")));
38        };
39        if verified.verified().claims().holder.kind != "person" {
40            return Ok(Err(refused(403, "agent_create_person_required")));
41        }
42        let http = reqwest::Client::builder()
43            .no_proxy()
44            .redirect(reqwest::redirect::Policy::none())
45            .build()
46            .map_err(|_| DraftError::Lys("transport_failed".to_owned()))?;
47        let issuer = crate::auth_lys::live::lys_origin(
48            self.ask.as_deref().unwrap_or_else(|| self.adapter.issuer()),
49        )
50        .map_err(|_| DraftError::Contract)?;
51        let client = lys_pass::Client::new(http, issuer)
52            .map_err(|error| DraftError::Lys(error.name().to_owned()))?;
53        Ok(Ok(OutrightAsk { verified, client }))
54    }
55}
56
57impl OutrightAsk {
58    /// Asks Lys whether the pass's holder holds `target` outright; answers the pass and its
59    /// effective grant, or the refusal.
60    ///
61    /// # Errors
62    /// Lys could not be asked.
63    pub async fn check(
64        self,
65        target: &Target,
66        connector_pass: &str,
67    ) -> Result<
68        Result<(crate::auth_lys::lys_pass_adapter::LysPass, String), AdminResponse>,
69        DraftError,
70    > {
71        let holder = self.verified.verified().claims().holder.id.clone();
72        let answer = self
73            .client
74            .check_batch(connector_pass, &holder, std::slice::from_ref(target), None)
75            .await
76            .map_err(|error| DraftError::Lys(error.name().to_owned()))?;
77        let Some(result) = answer.results.first() else {
78            return Err(DraftError::Contract);
79        };
80        match (&result.grant, result.allowed, result.mode) {
81            (Some(grant), true, Some(lys_pass::Mode::Outright)) => {
82                Ok(Ok((self.verified, grant.clone())))
83            }
84            _ => Ok(Err(AdminResponse {
85                status: 403,
86                body: json!({"refusal": result.refusal.clone().unwrap_or_else(|| "agent_create_outright_required".to_owned())}),
87            })),
88        }
89    }
90}