1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! Approved Lys drafts applied to the served store exactly once
//! (ACCESS-001 R3, PERMISSIONS-ACROSS-THE-STACK D4).
//!
//! A held change was drafted with the exact frame its client sent. Once Lys
//! approves it, the connector pulls it and lys-pass's `execute_once` hands it
//! here. Its words are decoded as the transport decodes a frame, and must
//! name exactly the resource and action the draft was approved for, so an
//! approval never applies an act on anything else. The change is then
//! applied through the service's own mutation path. That path is
//! receipt-first: a request id already applied answers its stored receipt
//! and writes nothing. So the mutation and its receipt are one commit, and
//! a second run after a lost acknowledgment applies nothing again.
use haematite::tree::Hash;
use lys_pass::drafts::{ApprovedDraft, Execution, Executor};
use crate::serve::authorize::subjects;
use crate::serve::pass::target;
use crate::serve::protocol::{Operation, Request};
use crate::serve::service::Service;
use crate::serve::storage::Failure;
/// The served store's draft ledger, for the database `database`.
pub struct Drafts<'a> {
service: &'a Service,
database: String,
}
impl<'a> Drafts<'a> {
/// Apply approved drafts to `service`, the served database `database`.
#[must_use]
pub fn new(service: &'a Service, database: &str) -> Self {
Self {
service,
database: database.to_owned(),
}
}
}
impl Executor for Drafts<'_> {
type Error = Failure;
/// The outcome is keyed by the branch and request id inside the draft's
/// words, which an id alone does not carry, so it is read where the
/// change is applied: receipt-first, before anything is written.
fn receipt(&self, _draft: &str) -> Result<Option<Execution>, Failure> {
Ok(None)
}
/// A draft whose words are not a change on exactly the approved
/// resource is refused on execution by its own name, so Lys records the
/// refusal and the page moves on. A failure of the store itself stays an
/// error: nothing is acknowledged, and the next pull asks again.
fn execute_and_record(&mut self, draft: &ApprovedDraft) -> Result<Execution, Failure> {
let operation = match self.approved_change(draft) {
Ok(operation) => operation,
Err(refusal) => {
return Ok(Execution::RefusedOnExecution {
request_digest: draft.request_digest.clone(),
refusal: refusal.code,
reason: refusal.message,
});
}
};
let receipt = self
.service
.dispatch(operation)
.map_err(|failure| failure.failure().clone())?;
let bytes = serde_json::to_vec(&receipt).map_err(Failure::storage)?;
Ok(Execution::Executed {
request_digest: draft.request_digest.clone(),
receipt_digest: Hash::of(&bytes).to_string(),
})
}
}
impl Drafts<'_> {
/// The change in `draft`'s words, when they name exactly the resource and
/// action the draft was approved for; otherwise the refusal that says why.
fn approved_change(&self, draft: &ApprovedDraft) -> Result<Operation, Failure> {
let operation = serde_json::from_str::<Request>(&draft.words)
.map_err(|error| Failure::new("draft_words_invalid", error.to_string()))?
.operation;
if !matches!(operation, Operation::Mutate(_)) {
return Err(Failure::new(
"draft_not_a_change",
"only a change is applied from an approved draft",
));
}
let differs = || {
Failure::new(
"draft_target_differs",
"the draft's words act on another resource than it was approved for",
)
};
let asked = subjects(&operation)?;
let [subject] = asked.as_slice() else {
return Err(differs());
};
let named = target(subject.route, &self.database, subject.name.as_deref())
.map_err(|error| Failure::new(error.name(), "the draft's resource is invalid"))?;
if named != draft.target {
return Err(differs());
}
Ok(operation)
}
}
#[cfg(test)]
#[path = "drafts_tests.rs"]
mod tests;