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 {
145 let origin = origin_header.and_then(TaskOrigin::parse);
146 match origin {
147 Some(origin @ (TaskOrigin::Peer | TaskOrigin::Completion)) => {
148 TaskProvenance::derive(origin, trust_header.and_then(TrustClass::parse))
149 }
150 _ => TaskProvenance::derive(TaskOrigin::Event, None),
151 }
152}
153
154pub fn stamp_for_peer(sender_task: Option<TaskProvenance>) -> TaskProvenance {
161 TaskProvenance::derive(TaskOrigin::Peer, sender_task.map(|task| task.trust()))
162}
163
164pub fn stamp_for_completion(inherited: Option<TrustClass>) -> TaskProvenance {
172 TaskProvenance::derive(TaskOrigin::Completion, inherited)
173}
174
175pub fn artifact_trust(origin: &murmur_artifact::LockOrigin) -> TrustClass {
185 match origin {
186 murmur_artifact::LockOrigin::Operator => TrustClass::Trusted,
187 murmur_artifact::LockOrigin::Runtime { .. } => TrustClass::Untrusted,
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 const ALL_ORIGINS: [TaskOrigin; 6] = [
196 TaskOrigin::User,
197 TaskOrigin::Peer,
198 TaskOrigin::Schedule,
199 TaskOrigin::Event,
200 TaskOrigin::Completion,
201 TaskOrigin::System,
202 ];
203
204 #[test]
205 fn origin_round_trips_through_its_wire_spelling() {
206 for origin in ALL_ORIGINS {
207 assert_eq!(TaskOrigin::parse(origin.as_str()), Some(origin));
208 }
209 assert_eq!(TaskOrigin::parse("User"), None);
210 assert_eq!(TaskOrigin::parse("not-an-origin"), None);
211 assert_eq!(TaskOrigin::parse(""), None);
212 }
213
214 #[test]
215 fn trust_round_trips_through_its_wire_spelling() {
216 for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
217 assert_eq!(TrustClass::parse(trust.as_str()), Some(trust));
218 }
219 assert_eq!(TrustClass::parse("Trusted"), None);
220 assert_eq!(TrustClass::parse("maybe"), None);
221 }
222
223 #[test]
225 fn derive_covers_every_origin_and_inherited_pair() {
226 let cases: [(TaskOrigin, Option<TrustClass>, TrustClass); 18] = [
227 (TaskOrigin::User, None, TrustClass::Trusted),
228 (
229 TaskOrigin::User,
230 Some(TrustClass::Trusted),
231 TrustClass::Trusted,
232 ),
233 (
234 TaskOrigin::User,
235 Some(TrustClass::Untrusted),
236 TrustClass::Trusted,
237 ),
238 (TaskOrigin::Schedule, None, TrustClass::Trusted),
239 (
240 TaskOrigin::Schedule,
241 Some(TrustClass::Trusted),
242 TrustClass::Trusted,
243 ),
244 (
245 TaskOrigin::Schedule,
246 Some(TrustClass::Untrusted),
247 TrustClass::Trusted,
248 ),
249 (TaskOrigin::System, None, TrustClass::Trusted),
250 (
251 TaskOrigin::System,
252 Some(TrustClass::Trusted),
253 TrustClass::Trusted,
254 ),
255 (
256 TaskOrigin::System,
257 Some(TrustClass::Untrusted),
258 TrustClass::Trusted,
259 ),
260 (TaskOrigin::Event, None, TrustClass::Untrusted),
261 (
262 TaskOrigin::Event,
263 Some(TrustClass::Trusted),
264 TrustClass::Untrusted,
265 ),
266 (
267 TaskOrigin::Event,
268 Some(TrustClass::Untrusted),
269 TrustClass::Untrusted,
270 ),
271 (TaskOrigin::Peer, None, TrustClass::Untrusted),
272 (
273 TaskOrigin::Peer,
274 Some(TrustClass::Trusted),
275 TrustClass::Trusted,
276 ),
277 (
278 TaskOrigin::Peer,
279 Some(TrustClass::Untrusted),
280 TrustClass::Untrusted,
281 ),
282 (TaskOrigin::Completion, None, TrustClass::Untrusted),
283 (
284 TaskOrigin::Completion,
285 Some(TrustClass::Trusted),
286 TrustClass::Trusted,
287 ),
288 (
289 TaskOrigin::Completion,
290 Some(TrustClass::Untrusted),
291 TrustClass::Untrusted,
292 ),
293 ];
294 for (origin, inherited, expected) in cases {
295 let provenance = TaskProvenance::derive(origin, inherited);
296 assert_eq!(
297 provenance.trust(),
298 expected,
299 "{origin:?} with {inherited:?} should be {expected:?}"
300 );
301 assert_eq!(provenance.origin(), origin);
302 }
303 }
304
305 #[test]
306 fn wire_accepts_only_peer_and_completion() {
307 for accepted in ["peer", "completion"] {
308 let provenance = from_wire(Some(accepted), Some("trusted"));
309 assert_eq!(provenance.origin(), TaskOrigin::parse(accepted).unwrap());
310 assert_eq!(provenance.trust(), TrustClass::Trusted);
311 }
312 }
313
314 #[test]
315 fn wire_refuses_locally_only_and_unrecognised_origins() {
316 for refused in ["user", "schedule", "event", "system", "not-an-origin", ""] {
317 let provenance = from_wire(Some(refused), Some("trusted"));
318 assert_eq!(
319 provenance.origin(),
320 TaskOrigin::Event,
321 "{refused:?} must not be accepted from the wire"
322 );
323 assert_eq!(provenance.trust(), TrustClass::Untrusted);
324 }
325 }
326
327 #[test]
328 fn wire_without_an_origin_header_is_an_untrusted_event() {
329 let provenance = from_wire(None, Some("trusted"));
330 assert_eq!(provenance.origin(), TaskOrigin::Event);
331 assert_eq!(provenance.trust(), TrustClass::Untrusted);
332 }
333
334 #[test]
335 fn wire_trust_defaults_to_untrusted_when_absent_or_unparseable() {
336 for trust_header in [None, Some("Trusted"), Some("yes"), Some("")] {
337 let provenance = from_wire(Some("peer"), trust_header);
338 assert_eq!(provenance.origin(), TaskOrigin::Peer);
339 assert_eq!(
340 provenance.trust(),
341 TrustClass::Untrusted,
342 "trust header {trust_header:?} must not resolve to trusted"
343 );
344 }
345 }
346
347 #[test]
348 fn stamp_for_peer_carries_the_senders_class_and_defaults_untrusted() {
349 assert_eq!(stamp_for_peer(None).origin(), TaskOrigin::Peer);
350 assert_eq!(stamp_for_peer(None).trust(), TrustClass::Untrusted);
351 for (sender, expected) in [
352 (TaskOrigin::User, TrustClass::Trusted),
353 (TaskOrigin::Event, TrustClass::Untrusted),
354 ] {
355 let stamped = stamp_for_peer(Some(TaskProvenance::derive(sender, None)));
356 assert_eq!(stamped.origin(), TaskOrigin::Peer);
357 assert_eq!(stamped.trust(), expected);
358 }
359 }
360
361 #[test]
363 fn stamp_for_completion_carries_the_delegating_tasks_class() {
364 assert_eq!(stamp_for_completion(None).origin(), TaskOrigin::Completion);
365 assert_eq!(stamp_for_completion(None).trust(), TrustClass::Untrusted);
366 for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
367 let stamped = stamp_for_completion(Some(trust));
368 assert_eq!(stamped.origin(), TaskOrigin::Completion);
369 assert_eq!(stamped.trust(), trust);
370 }
371 }
372
373 #[test]
374 fn artifact_trust_of_an_operator_pin_is_trusted() {
375 assert_eq!(
376 artifact_trust(&murmur_artifact::LockOrigin::Operator),
377 TrustClass::Trusted
378 );
379 }
380
381 #[test]
382 fn artifact_trust_of_a_runtime_pin_is_untrusted_for_any_session() {
383 for session in ["ses_puller", "", "operator", "trusted"] {
384 let origin = murmur_artifact::LockOrigin::Runtime {
385 session: session.to_string(),
386 };
387 assert_eq!(
388 artifact_trust(&origin),
389 TrustClass::Untrusted,
390 "session {session:?} must not make a runtime pin trusted"
391 );
392 }
393 }
394
395 #[test]
396 fn wire_untrusted_peer_stays_untrusted() {
397 let provenance = from_wire(Some("peer"), Some("untrusted"));
398 assert_eq!(provenance.origin(), TaskOrigin::Peer);
399 assert_eq!(provenance.trust(), TrustClass::Untrusted);
400 }
401}