1pub const PEER_ORIGIN_HEADER: &str = "x-murmur-task-origin";
19
20pub const PEER_TRUST_HEADER: &str = "x-murmur-task-trust";
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TaskOrigin {
26 User,
28 Peer,
30 Schedule,
32 Event,
34 Completion,
37 System,
39}
40
41impl TaskOrigin {
42 pub fn as_str(&self) -> &'static str {
44 match self {
45 Self::User => "user",
46 Self::Peer => "peer",
47 Self::Schedule => "schedule",
48 Self::Event => "event",
49 Self::Completion => "completion",
50 Self::System => "system",
51 }
52 }
53
54 pub fn parse(value: &str) -> Option<Self> {
57 match value {
58 "user" => Some(Self::User),
59 "peer" => Some(Self::Peer),
60 "schedule" => Some(Self::Schedule),
61 "event" => Some(Self::Event),
62 "completion" => Some(Self::Completion),
63 "system" => Some(Self::System),
64 _ => None,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum TrustClass {
72 Trusted,
73 Untrusted,
74}
75
76impl TrustClass {
77 pub fn as_str(&self) -> &'static str {
79 match self {
80 Self::Trusted => "trusted",
81 Self::Untrusted => "untrusted",
82 }
83 }
84
85 pub fn parse(value: &str) -> Option<Self> {
87 match value {
88 "trusted" => Some(Self::Trusted),
89 "untrusted" => Some(Self::Untrusted),
90 _ => None,
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct TaskProvenance {
101 origin: TaskOrigin,
102 trust: TrustClass,
103}
104
105impl TaskProvenance {
106 pub fn derive(origin: TaskOrigin, inherited: Option<TrustClass>) -> Self {
114 let trust = match origin {
115 TaskOrigin::User | TaskOrigin::Schedule | TaskOrigin::System => TrustClass::Trusted,
116 TaskOrigin::Event => TrustClass::Untrusted,
117 TaskOrigin::Peer | TaskOrigin::Completion => inherited.unwrap_or(TrustClass::Untrusted),
118 };
119 Self { origin, trust }
120 }
121
122 pub fn origin(&self) -> TaskOrigin {
123 self.origin
124 }
125
126 pub fn trust(&self) -> TrustClass {
127 self.trust
128 }
129}
130
131pub fn from_wire(origin_header: Option<&str>, trust_header: Option<&str>) -> TaskProvenance {
143 let origin = origin_header.and_then(TaskOrigin::parse);
144 match origin {
145 Some(origin @ (TaskOrigin::Peer | TaskOrigin::Completion)) => {
146 TaskProvenance::derive(origin, trust_header.and_then(TrustClass::parse))
147 }
148 _ => TaskProvenance::derive(TaskOrigin::Event, None),
149 }
150}
151
152pub fn stamp_for_peer(sender_task: Option<TaskProvenance>) -> TaskProvenance {
159 TaskProvenance::derive(TaskOrigin::Peer, sender_task.map(|task| task.trust()))
160}
161
162pub fn stamp_for_completion(inherited: Option<TrustClass>) -> TaskProvenance {
170 TaskProvenance::derive(TaskOrigin::Completion, inherited)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 const ALL_ORIGINS: [TaskOrigin; 6] = [
178 TaskOrigin::User,
179 TaskOrigin::Peer,
180 TaskOrigin::Schedule,
181 TaskOrigin::Event,
182 TaskOrigin::Completion,
183 TaskOrigin::System,
184 ];
185
186 #[test]
187 fn origin_round_trips_through_its_wire_spelling() {
188 for origin in ALL_ORIGINS {
189 assert_eq!(TaskOrigin::parse(origin.as_str()), Some(origin));
190 }
191 assert_eq!(TaskOrigin::parse("User"), None);
192 assert_eq!(TaskOrigin::parse("not-an-origin"), None);
193 assert_eq!(TaskOrigin::parse(""), None);
194 }
195
196 #[test]
197 fn trust_round_trips_through_its_wire_spelling() {
198 for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
199 assert_eq!(TrustClass::parse(trust.as_str()), Some(trust));
200 }
201 assert_eq!(TrustClass::parse("Trusted"), None);
202 assert_eq!(TrustClass::parse("maybe"), None);
203 }
204
205 #[test]
207 fn derive_covers_every_origin_and_inherited_pair() {
208 let cases: [(TaskOrigin, Option<TrustClass>, TrustClass); 18] = [
209 (TaskOrigin::User, None, TrustClass::Trusted),
210 (
211 TaskOrigin::User,
212 Some(TrustClass::Trusted),
213 TrustClass::Trusted,
214 ),
215 (
216 TaskOrigin::User,
217 Some(TrustClass::Untrusted),
218 TrustClass::Trusted,
219 ),
220 (TaskOrigin::Schedule, None, TrustClass::Trusted),
221 (
222 TaskOrigin::Schedule,
223 Some(TrustClass::Trusted),
224 TrustClass::Trusted,
225 ),
226 (
227 TaskOrigin::Schedule,
228 Some(TrustClass::Untrusted),
229 TrustClass::Trusted,
230 ),
231 (TaskOrigin::System, None, TrustClass::Trusted),
232 (
233 TaskOrigin::System,
234 Some(TrustClass::Trusted),
235 TrustClass::Trusted,
236 ),
237 (
238 TaskOrigin::System,
239 Some(TrustClass::Untrusted),
240 TrustClass::Trusted,
241 ),
242 (TaskOrigin::Event, None, TrustClass::Untrusted),
243 (
244 TaskOrigin::Event,
245 Some(TrustClass::Trusted),
246 TrustClass::Untrusted,
247 ),
248 (
249 TaskOrigin::Event,
250 Some(TrustClass::Untrusted),
251 TrustClass::Untrusted,
252 ),
253 (TaskOrigin::Peer, None, TrustClass::Untrusted),
254 (
255 TaskOrigin::Peer,
256 Some(TrustClass::Trusted),
257 TrustClass::Trusted,
258 ),
259 (
260 TaskOrigin::Peer,
261 Some(TrustClass::Untrusted),
262 TrustClass::Untrusted,
263 ),
264 (TaskOrigin::Completion, None, TrustClass::Untrusted),
265 (
266 TaskOrigin::Completion,
267 Some(TrustClass::Trusted),
268 TrustClass::Trusted,
269 ),
270 (
271 TaskOrigin::Completion,
272 Some(TrustClass::Untrusted),
273 TrustClass::Untrusted,
274 ),
275 ];
276 for (origin, inherited, expected) in cases {
277 let provenance = TaskProvenance::derive(origin, inherited);
278 assert_eq!(
279 provenance.trust(),
280 expected,
281 "{origin:?} with {inherited:?} should be {expected:?}"
282 );
283 assert_eq!(provenance.origin(), origin);
284 }
285 }
286
287 #[test]
288 fn wire_accepts_only_peer_and_completion() {
289 for accepted in ["peer", "completion"] {
290 let provenance = from_wire(Some(accepted), Some("trusted"));
291 assert_eq!(provenance.origin(), TaskOrigin::parse(accepted).unwrap());
292 assert_eq!(provenance.trust(), TrustClass::Trusted);
293 }
294 }
295
296 #[test]
297 fn wire_refuses_locally_only_and_unrecognised_origins() {
298 for refused in ["user", "schedule", "event", "system", "not-an-origin", ""] {
299 let provenance = from_wire(Some(refused), Some("trusted"));
300 assert_eq!(
301 provenance.origin(),
302 TaskOrigin::Event,
303 "{refused:?} must not be accepted from the wire"
304 );
305 assert_eq!(provenance.trust(), TrustClass::Untrusted);
306 }
307 }
308
309 #[test]
310 fn wire_without_an_origin_header_is_an_untrusted_event() {
311 let provenance = from_wire(None, Some("trusted"));
312 assert_eq!(provenance.origin(), TaskOrigin::Event);
313 assert_eq!(provenance.trust(), TrustClass::Untrusted);
314 }
315
316 #[test]
317 fn wire_trust_defaults_to_untrusted_when_absent_or_unparseable() {
318 for trust_header in [None, Some("Trusted"), Some("yes"), Some("")] {
319 let provenance = from_wire(Some("peer"), trust_header);
320 assert_eq!(provenance.origin(), TaskOrigin::Peer);
321 assert_eq!(
322 provenance.trust(),
323 TrustClass::Untrusted,
324 "trust header {trust_header:?} must not resolve to trusted"
325 );
326 }
327 }
328
329 #[test]
330 fn stamp_for_peer_carries_the_senders_class_and_defaults_untrusted() {
331 assert_eq!(stamp_for_peer(None).origin(), TaskOrigin::Peer);
332 assert_eq!(stamp_for_peer(None).trust(), TrustClass::Untrusted);
333 for (sender, expected) in [
334 (TaskOrigin::User, TrustClass::Trusted),
335 (TaskOrigin::Event, TrustClass::Untrusted),
336 ] {
337 let stamped = stamp_for_peer(Some(TaskProvenance::derive(sender, None)));
338 assert_eq!(stamped.origin(), TaskOrigin::Peer);
339 assert_eq!(stamped.trust(), expected);
340 }
341 }
342
343 #[test]
345 fn stamp_for_completion_carries_the_delegating_tasks_class() {
346 assert_eq!(stamp_for_completion(None).origin(), TaskOrigin::Completion);
347 assert_eq!(stamp_for_completion(None).trust(), TrustClass::Untrusted);
348 for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
349 let stamped = stamp_for_completion(Some(trust));
350 assert_eq!(stamped.origin(), TaskOrigin::Completion);
351 assert_eq!(stamped.trust(), trust);
352 }
353 }
354
355 #[test]
356 fn wire_untrusted_peer_stays_untrusted() {
357 let provenance = from_wire(Some("peer"), Some("untrusted"));
358 assert_eq!(provenance.origin(), TaskOrigin::Peer);
359 assert_eq!(provenance.trust(), TrustClass::Untrusted);
360 }
361}