liminal_server/services/site/
executor.rs1use lys_pass::drafts::{ApprovedDraft, DraftRequest, Execution};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeSet;
6use std::num::NonZeroUsize;
7use std::sync::Arc;
8
9use super::super::Lifecycle;
10use super::{SiteAct, SiteError};
11use crate::auth_lys::live::drafts::{
12 ApprovalEvidence, ApprovalTransition, AtomicDraftStore, DraftError,
13};
14
15#[derive(Clone, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub(in super::super) struct Prepared {
19 pub draft: ApprovedDraft,
20 pub act: SiteAct,
21 pub at_ms: u64,
22}
23
24#[derive(Clone, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub(in super::super) struct Outcome {
28 pub receipt: Execution,
29 pub approval: ApprovalTransition,
30 pub acknowledged: bool,
31 pub delivered: bool,
32}
33
34pub trait ApprovalReader: Send + Sync {
36 fn read(&self, draft: &ApprovedDraft) -> Result<ApprovalEvidence, DraftError>;
41}
42
43pub trait OutcomeDelivery: Send + Sync {
45 fn deliver(
50 &self,
51 holder: &str,
52 draft: &ApprovedDraft,
53 receipt: &Execution,
54 ) -> Result<(), DraftError>;
55}
56
57pub struct SiteDraftStore {
59 lifecycle: Arc<Lifecycle>,
60 approvals: Arc<dyn ApprovalReader>,
61 delivery: Arc<dyn OutcomeDelivery>,
62 at_ms: u64,
63}
64
65impl SiteDraftStore {
66 #[must_use]
68 pub fn new(
69 lifecycle: Arc<Lifecycle>,
70 approvals: Arc<dyn ApprovalReader>,
71 delivery: Arc<dyn OutcomeDelivery>,
72 at_ms: u64,
73 ) -> Self {
74 Self {
75 lifecycle,
76 approvals,
77 delivery,
78 at_ms,
79 }
80 }
81
82 fn validate_approval(
83 &self,
84 draft: &ApprovedDraft,
85 transition: &ApprovalTransition,
86 ) -> Result<(), DraftError> {
87 let evidence = self.approvals.read(draft)?;
88 let needed = match evidence.mode {
89 crate::auth_lys::GrantMode::ByDraft => 1,
90 crate::auth_lys::GrantMode::ByTwo => 2,
91 crate::auth_lys::GrantMode::Outright => return Err(DraftError::ApprovalUnavailable),
92 };
93 let limits = &self
94 .lifecycle
95 .site_bindings()
96 .map_err(storage_error)?
97 .limits;
98 let mut people = BTreeSet::new();
99 if evidence.draft != draft.id
100 || evidence.digest != draft.request_digest
101 || evidence.approved_by != transition.approved_by
102 || evidence.approved_by.len() != needed
103 || evidence.approved_by.len() > limits.approvers.get()
104 || evidence.approved_by.iter().any(|person| {
105 person == "lys"
106 || person.is_empty()
107 || person.len() > limits.text_bytes.get()
108 || person.chars().any(char::is_control)
109 || !people.insert(person)
110 })
111 {
112 return Err(DraftError::ApprovalUnavailable);
113 }
114 Ok(())
115 }
116
117 pub fn receipt(&self, id: &str) -> Result<Option<Execution>, DraftError> {
122 AtomicDraftStore::receipt(self, id)
123 }
124
125 pub const fn observe_execution_time(&mut self, at_ms: u64) {
127 self.at_ms = at_ms;
128 }
129
130 pub fn execute_approved(&mut self, draft: &ApprovedDraft) -> Result<Execution, DraftError> {
136 {
137 let registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
138 let prepared = registry
139 .construction
140 .drafts
141 .get(&draft.id)
142 .ok_or(DraftError::Contract)?;
143 if !same_draft(&prepared.draft, draft) {
144 return Err(DraftError::ReceiptMismatch);
145 }
146 drop(registry);
147 }
148 let receipt = lys_pass::drafts::execute_once(draft, self).map_err(|error| match error {
149 lys_pass::drafts::ExecuteError::DigestMismatch => DraftError::ReceiptMismatch,
150 lys_pass::drafts::ExecuteError::Product(error) => error,
151 lys_pass::drafts::ExecuteError::Contract(_) => DraftError::Contract,
152 })?;
153 self.lifecycle.refresh_sites().map_err(storage_error)?;
154 Ok(receipt)
155 }
156}
157
158impl Lifecycle {
159 pub fn keep_held_site(
165 &self,
166 id: &str,
167 request: &DraftRequest,
168 act: &SiteAct,
169 pass: &lys_pass::VerifiedPass,
170 mode: crate::auth_lys::GrantMode,
171 at_ms: u64,
172 ) -> Result<(), SiteError> {
173 let bindings = self.site_bindings()?;
174 let bound = bindings.limits.text_bytes.get();
175 if [
176 id,
177 &request.grant,
178 &act.target,
179 &act.holder,
180 &act.responsible,
181 ]
182 .iter()
183 .any(|value| value.is_empty() || value.len() > bound || value.chars().any(char::is_control))
184 {
185 return Err(SiteError::Invalid("held metadata"));
186 }
187 let verified = SiteAct::prepare(act.target.clone(), act.change.clone(), pass)?;
188 let exact = serde_json::to_string(&verified).map_err(|_| SiteError::Store)?;
189 if exact != request.words
190 || request.words.len() > bindings.limits.request_bytes.get()
191 || request.request_digest != lys_pass::drafts::request_digest(&exact)
192 || request.target.kind != act.change.kind()
193 || request.target.id != act.permission_id(self.network_id())
194 || request.target.action != act.change.action()
195 {
196 return Err(SiteError::Invalid("held immutable request"));
197 }
198 let mode = match mode {
200 crate::auth_lys::GrantMode::ByDraft => lys_pass::Mode::ByDraft,
201 crate::auth_lys::GrantMode::ByTwo => lys_pass::Mode::ByTwo,
202 crate::auth_lys::GrantMode::Outright => {
203 return Err(SiteError::Invalid("held approval mode"));
204 }
205 };
206 let draft = ApprovedDraft {
207 id: id.into(),
208 app: "liminal".into(),
209 grant: request.grant.clone(),
210 target: request.target.clone(),
211 request_digest: request.request_digest.clone(),
212 approved_by: Vec::new(),
213 mode,
214 words: exact,
215 };
216 let mut registry = self.lock()?;
217 if let Some(old) = registry.construction.drafts.get(id) {
218 return if same_draft(&old.draft, &draft) {
219 Ok(())
220 } else {
221 Err(SiteError::Invalid("held draft conflict"))
222 };
223 }
224 if registry.construction.drafts.len() >= bindings.limits.drafts.get() {
225 return Err(SiteError::Bound("drafts"));
226 }
227 let mut snapshot = Self::snapshot(®istry);
228 snapshot.construction.drafts.insert(
229 id.into(),
230 Prepared {
231 draft,
232 act: verified,
233 at_ms,
234 },
235 );
236 super::storage::commit(self, &mut registry, &snapshot)?;
237 drop(registry);
238 Ok(())
239 }
240}
241
242fn same_draft(left: &ApprovedDraft, right: &ApprovedDraft) -> bool {
243 left.id == right.id
244 && left.app == right.app
245 && left.grant == right.grant
246 && left.target.kind == right.target.kind
247 && left.target.id == right.target.id
248 && left.target.action == right.target.action
249 && left.request_digest == right.request_digest
250 && left.words == right.words
251}
252
253fn storage_error(_: SiteError) -> DraftError {
254 DraftError::Store
255}
256
257impl AtomicDraftStore for SiteDraftStore {
258 fn receipt(&self, draft: &str) -> Result<Option<Execution>, DraftError> {
259 Ok(self
260 .lifecycle
261 .lock()
262 .map_err(|_| DraftError::Store)?
263 .construction
264 .outcomes
265 .get(draft)
266 .map(|outcome| outcome.receipt.clone()))
267 }
268 fn approval(&self, draft: &ApprovedDraft) -> Result<ApprovalEvidence, DraftError> {
269 self.approvals.read(draft)
270 }
271 fn commit(
272 &mut self,
273 draft: &ApprovedDraft,
274 transition: &ApprovalTransition,
275 ) -> Result<Execution, DraftError> {
276 self.validate_approval(draft, transition)?;
277 let mut registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
278 if let Some(outcome) = registry.construction.outcomes.get(&draft.id) {
279 let prepared = registry
280 .construction
281 .drafts
282 .get(&draft.id)
283 .ok_or(DraftError::Contract)?;
284 if !same_draft(&prepared.draft, draft) {
285 return Err(DraftError::ReceiptMismatch);
286 }
287 return Ok(outcome.receipt.clone());
288 }
289 let prepared = registry
290 .construction
291 .drafts
292 .get(&draft.id)
293 .cloned()
294 .ok_or(DraftError::Contract)?;
295 if !same_draft(&prepared.draft, draft)
296 || transition.draft != draft.id
297 || transition.digest != draft.request_digest
298 || transition.approved_by.is_empty()
299 {
300 return Err(DraftError::ReceiptMismatch);
301 }
302 let mut snapshot = Lifecycle::snapshot(®istry);
303 let applied = self.lifecycle.apply_site_act(
304 &mut snapshot,
305 &prepared.act,
306 transition,
307 prepared.at_ms,
308 ®istry.trials,
309 );
310 let receipt = match applied {
311 Ok(()) => {
312 let bytes = serde_json::to_string(&(
313 transition,
314 &snapshot.records,
315 &snapshot.construction.templates,
316 ))
317 .map_err(|_| DraftError::Store)?;
318 Execution::Executed {
319 request_digest: draft.request_digest.clone(),
320 receipt_digest: lys_pass::drafts::request_digest(&bytes),
321 }
322 }
323 Err(SiteError::Pass(name)) => return Err(DraftError::Lys(name)),
324 Err(SiteError::Store | SiteError::Lifecycle(_)) => {
325 return Err(DraftError::Store);
326 }
327 Err(error) => {
328 snapshot = Lifecycle::snapshot(®istry);
329 Execution::RefusedOnExecution {
330 request_digest: draft.request_digest.clone(),
331 refusal: "site_execution_refused".into(),
332 reason: error.to_string(),
333 }
334 }
335 };
336 if matches!(receipt, Execution::Executed { .. }) {
337 for record in snapshot.records.values_mut() {
338 if let Some(approval) = record
339 .approvals
340 .last_mut()
341 .filter(|approval| approval.draft == draft.id)
342 {
343 approval.receipt = Some(receipt.clone());
344 }
345 }
346 for template in snapshot.construction.templates.values_mut() {
347 if let Some(approval) = template
348 .transitions
349 .last_mut()
350 .filter(|approval| approval.draft == draft.id)
351 {
352 approval.receipt = Some(receipt.clone());
353 }
354 }
355 }
356 snapshot.construction.outcomes.insert(
357 draft.id.clone(),
358 Outcome {
359 receipt: receipt.clone(),
360 approval: transition.clone(),
361 acknowledged: false,
362 delivered: false,
363 },
364 );
365 let result = super::storage::commit(&self.lifecycle, &mut registry, &snapshot)
366 .map_err(|_| DraftError::Store);
367 drop(registry);
368 result?;
369 self.lifecycle.refresh_sites().map_err(storage_error)?;
370 Ok(receipt)
371 }
372 fn acknowledged(&self, draft: &str) -> Result<bool, DraftError> {
373 Ok(self
374 .lifecycle
375 .lock()
376 .map_err(|_| DraftError::Store)?
377 .construction
378 .outcomes
379 .get(draft)
380 .is_some_and(|outcome| outcome.acknowledged))
381 }
382 fn acknowledge(&mut self, draft: &str, receipt: &Execution) -> Result<(), DraftError> {
383 let mut registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
384 let mut snapshot = Lifecycle::snapshot(®istry);
385 let outcome = snapshot
386 .construction
387 .outcomes
388 .get_mut(draft)
389 .ok_or(DraftError::ReceiptMismatch)?;
390 if &outcome.receipt != receipt {
391 return Err(DraftError::ReceiptMismatch);
392 }
393 if outcome.acknowledged {
394 return Ok(());
395 }
396 outcome.acknowledged = true;
397 let result = super::storage::commit(&self.lifecycle, &mut registry, &snapshot)
398 .map_err(|_| DraftError::Store);
399 drop(registry);
400 result
401 }
402 fn pending_acknowledgments(
403 &self,
404 limit: NonZeroUsize,
405 ) -> Result<Vec<ApprovedDraft>, DraftError> {
406 let registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
407 registry
408 .construction
409 .outcomes
410 .iter()
411 .filter(|(_, outcome)| !outcome.acknowledged)
412 .take(limit.get())
413 .map(|(id, _)| {
414 registry
415 .construction
416 .drafts
417 .get(id)
418 .map(|prepared| prepared.draft.clone())
419 .ok_or(DraftError::Store)
420 })
421 .collect()
422 }
423 fn deliver_outcome(
424 &mut self,
425 draft: &ApprovedDraft,
426 receipt: &Execution,
427 ) -> Result<(), DraftError> {
428 let (holder, delivered) = {
429 let registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
430 let outcome = registry
431 .construction
432 .outcomes
433 .get(&draft.id)
434 .ok_or(DraftError::ReceiptMismatch)?;
435 let prepared = registry
436 .construction
437 .drafts
438 .get(&draft.id)
439 .ok_or(DraftError::ReceiptMismatch)?;
440 if &outcome.receipt != receipt || !same_draft(&prepared.draft, draft) {
441 return Err(DraftError::ReceiptMismatch);
442 }
443 let result = (prepared.act.holder.clone(), outcome.delivered);
444 drop(registry);
445 result
446 };
447 if delivered {
448 return Ok(());
449 }
450 self.delivery.deliver(&holder, draft, receipt)?;
451 let mut registry = self.lifecycle.lock().map_err(|_| DraftError::Store)?;
452 let mut snapshot = Lifecycle::snapshot(®istry);
453 snapshot
454 .construction
455 .outcomes
456 .get_mut(&draft.id)
457 .ok_or(DraftError::Store)?
458 .delivered = true;
459 let result = super::storage::commit(&self.lifecycle, &mut registry, &snapshot)
460 .map_err(|_| DraftError::Store);
461 drop(registry);
462 result
463 }
464}
465
466#[path = "executor_run.rs"]
467mod run;