Skip to main content

liminal_server/auth_lys/
prepared.rs

1//! Prepared calls borrow the bearer inside its admitted authentication owner.
2
3use std::sync::Arc;
4
5use super::live::{HttpLysAsk, PreparedOwner};
6use super::lys_pass_adapter::LysPass;
7use crate::agents::waiting::durable::ApprovalAgents;
8use crate::services::lifecycle::{Lifecycle, site};
9
10/// Server-selected owners and immutable request data; carries no credential.
11pub struct SiteRequest<'a> {
12    pub lifecycle: &'a Arc<Lifecycle>,
13    pub owners: &'a Arc<ApprovalAgents>,
14    pub config: &'a crate::config::types::LysConfig,
15    pub handle: &'a tokio::runtime::Handle,
16    pub operation: &'a str,
17    pub target: &'a str,
18    pub change: site::SiteChange,
19    pub at_ms: u64,
20}
21
22/// A permission decision records exact words; it executes no network command.
23pub struct PermissionRequest<'a> {
24    pub lifecycle: &'a Arc<Lifecycle>,
25    pub owners: &'a Arc<ApprovalAgents>,
26    pub config: &'a crate::config::types::LysConfig,
27    pub handle: &'a tokio::runtime::Handle,
28    pub operation: &'a str,
29    pub words: &'a str,
30    pub at_ms: u64,
31}
32
33pub(crate) fn validate_permission_words(words: &str) -> Result<(), lys_pass::Error> {
34    #[derive(serde::Deserialize)]
35    #[serde(deny_unknown_fields)]
36    struct Words {
37        title: String,
38        options: Vec<OptionWord>,
39    }
40    #[derive(serde::Deserialize)]
41    #[serde(deny_unknown_fields, rename_all = "camelCase")]
42    struct OptionWord {
43        option_id: String,
44        name: String,
45        kind: String,
46    }
47    let value: Words =
48        serde_json::from_str(words).map_err(|_| lys_pass::Error::Invalid("permission words"))?;
49    let mut ids = std::collections::BTreeSet::new();
50    if value.title.trim().is_empty()
51        || value.options.is_empty()
52        || value.options.iter().any(|option| {
53            option.option_id.is_empty()
54                || option.name.is_empty()
55                || !ids.insert(&option.option_id)
56                || (option.kind.is_empty() || option.kind.chars().any(char::is_control))
57        })
58    {
59        return Err(lys_pass::Error::Invalid("permission options"));
60    }
61    Ok(())
62}
63
64impl PermissionRequest<'_> {
65    pub(super) fn execute(self, pass: &LysPass) -> Result<serde_json::Value, lys_pass::Error> {
66        validate_permission_words(self.words)?;
67        let limits = self
68            .config
69            .drafts
70            .ok_or(lys_pass::Error::CannotAsk("draft limits missing"))?;
71        let keeper = Arc::new(PreparedOwner::new(
72            self.owners.clone(),
73            self.lifecycle.clone(),
74            pass.shared_verified(),
75            limits.pull,
76            limits.waiting,
77        ));
78        let mut live = HttpLysAsk::new(
79            self.config
80                .api_url
81                .as_deref()
82                .unwrap_or(&self.config.issuer),
83            self.handle.clone(),
84        )?
85        .with_drafts(keeper);
86        if let Some(path) = &self.config.app_credential_path {
87            let credential = std::fs::read_to_string(path)
88                .map_err(|_| lys_pass::Error::CannotAsk("draft connector credential unreadable"))?;
89            live = live.asking_as(credential.trim().to_owned());
90        }
91        let target = lys_pass::Target::new("liminal.agent", pass.holder(), "agent_permission")?;
92        let answer = live.ask_prepared(
93            pass.raw_token(),
94            &super::live::PreparedAct {
95                holder: pass.holder(),
96                target: &target,
97                operation: self.operation,
98                words: self.words,
99                at_ms: self.at_ms,
100            },
101            limits.pull,
102        )?;
103        match answer.decision {
104            super::lys_pass_adapter::LiveAnswer::Held { draft, .. } => {
105                Ok(serde_json::json!({"state":"held","draft":draft,"words":self.words}))
106            }
107            _ => Err(lys_pass::Error::CannotAsk(
108                "permission requires a held draft",
109            )),
110        }
111    }
112}
113
114impl SiteRequest<'_> {
115    pub(super) fn execute(self, pass: &LysPass) -> Result<serde_json::Value, lys_pass::Error> {
116        let limits = self
117            .config
118            .drafts
119            .ok_or(lys_pass::Error::CannotAsk("draft limits missing"))?;
120        let keeper = Arc::new(PreparedOwner::new(
121            self.owners.clone(),
122            self.lifecycle.clone(),
123            pass.shared_verified(),
124            limits.pull,
125            limits.waiting,
126        ));
127        let mut live = HttpLysAsk::new(
128            self.config
129                .api_url
130                .as_deref()
131                .unwrap_or(&self.config.issuer),
132            self.handle.clone(),
133        )?
134        .with_drafts(keeper);
135        if let Some(path) = &self.config.app_credential_path {
136            let credential = std::fs::read_to_string(path)
137                .map_err(|_| lys_pass::Error::CannotAsk("draft connector credential unreadable"))?;
138            live = live.asking_as(credential.trim().to_owned());
139        }
140        site::preparation::hold_verified(
141            site::preparation::VerifiedContext {
142                lifecycle: self.lifecycle,
143                pass: pass.verified(),
144                token: pass.raw_token(),
145                live: &live,
146                at_ms: self.at_ms,
147                decisions: self.config.decisions,
148                warned: pass.warned(),
149                limits: limits.pull,
150            },
151            self.operation,
152            self.target,
153            self.change,
154        )
155        .map_err(|error| {
156            tracing::warn!(%error, "the construction act was not held");
157            lys_pass::Error::CannotAsk("prepared construction refused")
158        })
159    }
160}
161
162/// The site adapter forwards the admitted session; it cannot read its bearer.
163pub(crate) struct SiteHolder {
164    pub lifecycle: Arc<Lifecycle>,
165    pub owners: Arc<ApprovalAgents>,
166    pub config: crate::config::types::LysConfig,
167    pub handle: tokio::runtime::Handle,
168}
169
170impl std::fmt::Debug for SiteHolder {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("SiteHolder").finish_non_exhaustive()
173    }
174}
175
176impl crate::server::connection::services::SiteDraftHolder for SiteHolder {
177    fn hold(
178        &self,
179        lifecycle: &Lifecycle,
180        session: &dyn super::session::LysSession,
181        operation: &str,
182        target: &str,
183        change: site::SiteChange,
184        at_ms: u64,
185    ) -> Result<serde_json::Value, site::SiteError> {
186        if !std::ptr::eq(lifecycle, self.lifecycle.as_ref()) {
187            return Err(site::SiteError::Invalid("construction owner mismatch"));
188        }
189        session
190            .hold_site(SiteRequest {
191                lifecycle: &self.lifecycle,
192                owners: &self.owners,
193                config: &self.config,
194                handle: &self.handle,
195                operation,
196                target,
197                change,
198                at_ms,
199            })
200            .map_err(|error| {
201                // The caller hears the stable name; the operator's log names what failed.
202                tracing::warn!(target = %target, %error, "a construction draft could not be held");
203                site::SiteError::Pass(error.name().into())
204            })
205    }
206}