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,
39 System,
41}
42
43impl TaskOrigin {
44 pub fn as_str(&self) -> &'static str {
46 match self {
47 Self::User => "user",
48 Self::Peer => "peer",
49 Self::Schedule => "schedule",
50 Self::Event => "event",
51 Self::Completion => "completion",
52 Self::System => "system",
53 }
54 }
55
56 pub fn parse(value: &str) -> Option<Self> {
59 match value {
60 "user" => Some(Self::User),
61 "peer" => Some(Self::Peer),
62 "schedule" => Some(Self::Schedule),
63 "event" => Some(Self::Event),
64 "completion" => Some(Self::Completion),
65 "system" => Some(Self::System),
66 _ => None,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum TrustClass {
74 Trusted,
75 Untrusted,
76}
77
78impl TrustClass {
79 pub fn as_str(&self) -> &'static str {
81 match self {
82 Self::Trusted => "trusted",
83 Self::Untrusted => "untrusted",
84 }
85 }
86
87 pub fn parse(value: &str) -> Option<Self> {
89 match value {
90 "trusted" => Some(Self::Trusted),
91 "untrusted" => Some(Self::Untrusted),
92 _ => None,
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct TaskProvenance {
103 origin: TaskOrigin,
104 trust: TrustClass,
105}
106
107impl TaskProvenance {
108 pub fn derive(origin: TaskOrigin, inherited: Option<TrustClass>) -> Self {
116 let trust = match origin {
117 TaskOrigin::User | TaskOrigin::Schedule | TaskOrigin::System => TrustClass::Trusted,
118 TaskOrigin::Event => TrustClass::Untrusted,
119 TaskOrigin::Peer | TaskOrigin::Completion => inherited.unwrap_or(TrustClass::Untrusted),
120 };
121 Self { origin, trust }
122 }
123
124 pub fn origin(&self) -> TaskOrigin {
125 self.origin
126 }
127
128 pub fn trust(&self) -> TrustClass {
129 self.trust
130 }
131}
132
133pub fn from_wire(origin_header: Option<&str>, trust_header: Option<&str>) -> TaskProvenance {
150 let origin = origin_header.and_then(TaskOrigin::parse);
151 match origin {
152 Some(origin @ (TaskOrigin::Peer | TaskOrigin::Completion)) => {
153 TaskProvenance::derive(origin, trust_header.and_then(TrustClass::parse))
154 }
155 _ => TaskProvenance::derive(TaskOrigin::Event, None),
156 }
157}
158
159pub fn stamp_for_peer(sender_task: Option<TaskProvenance>) -> TaskProvenance {
166 TaskProvenance::derive(TaskOrigin::Peer, sender_task.map(|task| task.trust()))
167}
168
169pub fn artifact_trust(origin: &murmur_artifact::LockOrigin) -> TrustClass {
179 match origin {
180 murmur_artifact::LockOrigin::Operator => TrustClass::Trusted,
181 murmur_artifact::LockOrigin::Runtime { .. } => TrustClass::Untrusted,
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 const ALL_ORIGINS: [TaskOrigin; 6] = [
190 TaskOrigin::User,
191 TaskOrigin::Peer,
192 TaskOrigin::Schedule,
193 TaskOrigin::Event,
194 TaskOrigin::Completion,
195 TaskOrigin::System,
196 ];
197
198 #[test]
199 fn origin_round_trips_through_its_wire_spelling() {
200 for origin in ALL_ORIGINS {
201 assert_eq!(TaskOrigin::parse(origin.as_str()), Some(origin));
202 }
203 assert_eq!(TaskOrigin::parse("User"), None);
204 assert_eq!(TaskOrigin::parse("not-an-origin"), None);
205 assert_eq!(TaskOrigin::parse(""), None);
206 }
207
208 #[test]
209 fn trust_round_trips_through_its_wire_spelling() {
210 for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
211 assert_eq!(TrustClass::parse(trust.as_str()), Some(trust));
212 }
213 assert_eq!(TrustClass::parse("Trusted"), None);
214 assert_eq!(TrustClass::parse("maybe"), None);
215 }
216
217 #[test]
219 fn derive_covers_every_origin_and_inherited_pair() {
220 let cases: [(TaskOrigin, Option<TrustClass>, TrustClass); 18] = [
221 (TaskOrigin::User, None, TrustClass::Trusted),
222 (
223 TaskOrigin::User,
224 Some(TrustClass::Trusted),
225 TrustClass::Trusted,
226 ),
227 (
228 TaskOrigin::User,
229 Some(TrustClass::Untrusted),
230 TrustClass::Trusted,
231 ),
232 (TaskOrigin::Schedule, None, TrustClass::Trusted),
233 (
234 TaskOrigin::Schedule,
235 Some(TrustClass::Trusted),
236 TrustClass::Trusted,
237 ),
238 (
239 TaskOrigin::Schedule,
240 Some(TrustClass::Untrusted),
241 TrustClass::Trusted,
242 ),
243 (TaskOrigin::System, None, TrustClass::Trusted),
244 (
245 TaskOrigin::System,
246 Some(TrustClass::Trusted),
247 TrustClass::Trusted,
248 ),
249 (
250 TaskOrigin::System,
251 Some(TrustClass::Untrusted),
252 TrustClass::Trusted,
253 ),
254 (TaskOrigin::Event, None, TrustClass::Untrusted),
255 (
256 TaskOrigin::Event,
257 Some(TrustClass::Trusted),
258 TrustClass::Untrusted,
259 ),
260 (
261 TaskOrigin::Event,
262 Some(TrustClass::Untrusted),
263 TrustClass::Untrusted,
264 ),
265 (TaskOrigin::Peer, None, TrustClass::Untrusted),
266 (
267 TaskOrigin::Peer,
268 Some(TrustClass::Trusted),
269 TrustClass::Trusted,
270 ),
271 (
272 TaskOrigin::Peer,
273 Some(TrustClass::Untrusted),
274 TrustClass::Untrusted,
275 ),
276 (TaskOrigin::Completion, None, TrustClass::Untrusted),
277 (
278 TaskOrigin::Completion,
279 Some(TrustClass::Trusted),
280 TrustClass::Trusted,
281 ),
282 (
283 TaskOrigin::Completion,
284 Some(TrustClass::Untrusted),
285 TrustClass::Untrusted,
286 ),
287 ];
288 for (origin, inherited, expected) in cases {
289 let provenance = TaskProvenance::derive(origin, inherited);
290 assert_eq!(
291 provenance.trust(),
292 expected,
293 "{origin:?} with {inherited:?} should be {expected:?}"
294 );
295 assert_eq!(provenance.origin(), origin);
296 }
297 }
298
299 #[test]
300 fn wire_accepts_only_peer_and_completion() {
301 for accepted in ["peer", "completion"] {
302 let provenance = from_wire(Some(accepted), Some("trusted"));
303 assert_eq!(provenance.origin(), TaskOrigin::parse(accepted).unwrap());
304 assert_eq!(provenance.trust(), TrustClass::Trusted);
305 }
306 }
307
308 #[test]
309 fn wire_refuses_locally_only_and_unrecognised_origins() {
310 for refused in ["user", "schedule", "event", "system", "not-an-origin", ""] {
311 let provenance = from_wire(Some(refused), Some("trusted"));
312 assert_eq!(
313 provenance.origin(),
314 TaskOrigin::Event,
315 "{refused:?} must not be accepted from the wire"
316 );
317 assert_eq!(provenance.trust(), TrustClass::Untrusted);
318 }
319 }
320
321 #[test]
322 fn wire_without_an_origin_header_is_an_untrusted_event() {
323 let provenance = from_wire(None, Some("trusted"));
324 assert_eq!(provenance.origin(), TaskOrigin::Event);
325 assert_eq!(provenance.trust(), TrustClass::Untrusted);
326 }
327
328 #[test]
329 fn wire_trust_defaults_to_untrusted_when_absent_or_unparseable() {
330 for trust_header in [None, Some("Trusted"), Some("yes"), Some("")] {
331 let provenance = from_wire(Some("peer"), trust_header);
332 assert_eq!(provenance.origin(), TaskOrigin::Peer);
333 assert_eq!(
334 provenance.trust(),
335 TrustClass::Untrusted,
336 "trust header {trust_header:?} must not resolve to trusted"
337 );
338 }
339 }
340
341 #[test]
342 fn stamp_for_peer_carries_the_senders_class_and_defaults_untrusted() {
343 assert_eq!(stamp_for_peer(None).origin(), TaskOrigin::Peer);
344 assert_eq!(stamp_for_peer(None).trust(), TrustClass::Untrusted);
345 for (sender, expected) in [
346 (TaskOrigin::User, TrustClass::Trusted),
347 (TaskOrigin::Event, TrustClass::Untrusted),
348 ] {
349 let stamped = stamp_for_peer(Some(TaskProvenance::derive(sender, None)));
350 assert_eq!(stamped.origin(), TaskOrigin::Peer);
351 assert_eq!(stamped.trust(), expected);
352 }
353 }
354
355 #[test]
356 fn artifact_trust_of_an_operator_pin_is_trusted() {
357 assert_eq!(
358 artifact_trust(&murmur_artifact::LockOrigin::Operator),
359 TrustClass::Trusted
360 );
361 }
362
363 #[test]
364 fn artifact_trust_of_a_runtime_pin_is_untrusted_for_any_session() {
365 for session in ["ses_puller", "", "operator", "trusted"] {
366 let origin = murmur_artifact::LockOrigin::Runtime {
367 session: session.to_string(),
368 };
369 assert_eq!(
370 artifact_trust(&origin),
371 TrustClass::Untrusted,
372 "session {session:?} must not make a runtime pin trusted"
373 );
374 }
375 }
376
377 #[test]
378 fn wire_untrusted_peer_stays_untrusted() {
379 let provenance = from_wire(Some("peer"), Some("untrusted"));
380 assert_eq!(provenance.origin(), TaskOrigin::Peer);
381 assert_eq!(provenance.trust(), TrustClass::Untrusted);
382 }
383}