pub const PEER_ORIGIN_HEADER: &str = "x-murmur-task-origin";
pub const PEER_TRUST_HEADER: &str = "x-murmur-task-trust";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskOrigin {
User,
Peer,
Schedule,
Event,
Completion,
System,
}
impl TaskOrigin {
pub fn as_str(&self) -> &'static str {
match self {
Self::User => "user",
Self::Peer => "peer",
Self::Schedule => "schedule",
Self::Event => "event",
Self::Completion => "completion",
Self::System => "system",
}
}
pub fn parse(value: &str) -> Option<Self> {
match value {
"user" => Some(Self::User),
"peer" => Some(Self::Peer),
"schedule" => Some(Self::Schedule),
"event" => Some(Self::Event),
"completion" => Some(Self::Completion),
"system" => Some(Self::System),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustClass {
Trusted,
Untrusted,
}
impl TrustClass {
pub fn as_str(&self) -> &'static str {
match self {
Self::Trusted => "trusted",
Self::Untrusted => "untrusted",
}
}
pub fn parse(value: &str) -> Option<Self> {
match value {
"trusted" => Some(Self::Trusted),
"untrusted" => Some(Self::Untrusted),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TaskProvenance {
origin: TaskOrigin,
trust: TrustClass,
}
impl TaskProvenance {
pub fn derive(origin: TaskOrigin, inherited: Option<TrustClass>) -> Self {
let trust = match origin {
TaskOrigin::User | TaskOrigin::Schedule | TaskOrigin::System => TrustClass::Trusted,
TaskOrigin::Event => TrustClass::Untrusted,
TaskOrigin::Peer | TaskOrigin::Completion => inherited.unwrap_or(TrustClass::Untrusted),
};
Self { origin, trust }
}
pub fn origin(&self) -> TaskOrigin {
self.origin
}
pub fn trust(&self) -> TrustClass {
self.trust
}
}
pub fn from_wire(origin_header: Option<&str>, trust_header: Option<&str>) -> TaskProvenance {
let origin = origin_header.and_then(TaskOrigin::parse);
match origin {
Some(origin @ (TaskOrigin::Peer | TaskOrigin::Completion)) => {
TaskProvenance::derive(origin, trust_header.and_then(TrustClass::parse))
}
_ => TaskProvenance::derive(TaskOrigin::Event, None),
}
}
pub fn stamp_for_peer(sender_task: Option<TaskProvenance>) -> TaskProvenance {
TaskProvenance::derive(TaskOrigin::Peer, sender_task.map(|task| task.trust()))
}
pub fn stamp_for_completion(inherited: Option<TrustClass>) -> TaskProvenance {
TaskProvenance::derive(TaskOrigin::Completion, inherited)
}
pub fn artifact_trust(origin: &murmur_artifact::LockOrigin) -> TrustClass {
match origin {
murmur_artifact::LockOrigin::Operator => TrustClass::Trusted,
murmur_artifact::LockOrigin::Runtime { .. } => TrustClass::Untrusted,
}
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_ORIGINS: [TaskOrigin; 6] = [
TaskOrigin::User,
TaskOrigin::Peer,
TaskOrigin::Schedule,
TaskOrigin::Event,
TaskOrigin::Completion,
TaskOrigin::System,
];
#[test]
fn origin_round_trips_through_its_wire_spelling() {
for origin in ALL_ORIGINS {
assert_eq!(TaskOrigin::parse(origin.as_str()), Some(origin));
}
assert_eq!(TaskOrigin::parse("User"), None);
assert_eq!(TaskOrigin::parse("not-an-origin"), None);
assert_eq!(TaskOrigin::parse(""), None);
}
#[test]
fn trust_round_trips_through_its_wire_spelling() {
for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
assert_eq!(TrustClass::parse(trust.as_str()), Some(trust));
}
assert_eq!(TrustClass::parse("Trusted"), None);
assert_eq!(TrustClass::parse("maybe"), None);
}
#[test]
fn derive_covers_every_origin_and_inherited_pair() {
let cases: [(TaskOrigin, Option<TrustClass>, TrustClass); 18] = [
(TaskOrigin::User, None, TrustClass::Trusted),
(
TaskOrigin::User,
Some(TrustClass::Trusted),
TrustClass::Trusted,
),
(
TaskOrigin::User,
Some(TrustClass::Untrusted),
TrustClass::Trusted,
),
(TaskOrigin::Schedule, None, TrustClass::Trusted),
(
TaskOrigin::Schedule,
Some(TrustClass::Trusted),
TrustClass::Trusted,
),
(
TaskOrigin::Schedule,
Some(TrustClass::Untrusted),
TrustClass::Trusted,
),
(TaskOrigin::System, None, TrustClass::Trusted),
(
TaskOrigin::System,
Some(TrustClass::Trusted),
TrustClass::Trusted,
),
(
TaskOrigin::System,
Some(TrustClass::Untrusted),
TrustClass::Trusted,
),
(TaskOrigin::Event, None, TrustClass::Untrusted),
(
TaskOrigin::Event,
Some(TrustClass::Trusted),
TrustClass::Untrusted,
),
(
TaskOrigin::Event,
Some(TrustClass::Untrusted),
TrustClass::Untrusted,
),
(TaskOrigin::Peer, None, TrustClass::Untrusted),
(
TaskOrigin::Peer,
Some(TrustClass::Trusted),
TrustClass::Trusted,
),
(
TaskOrigin::Peer,
Some(TrustClass::Untrusted),
TrustClass::Untrusted,
),
(TaskOrigin::Completion, None, TrustClass::Untrusted),
(
TaskOrigin::Completion,
Some(TrustClass::Trusted),
TrustClass::Trusted,
),
(
TaskOrigin::Completion,
Some(TrustClass::Untrusted),
TrustClass::Untrusted,
),
];
for (origin, inherited, expected) in cases {
let provenance = TaskProvenance::derive(origin, inherited);
assert_eq!(
provenance.trust(),
expected,
"{origin:?} with {inherited:?} should be {expected:?}"
);
assert_eq!(provenance.origin(), origin);
}
}
#[test]
fn wire_accepts_only_peer_and_completion() {
for accepted in ["peer", "completion"] {
let provenance = from_wire(Some(accepted), Some("trusted"));
assert_eq!(provenance.origin(), TaskOrigin::parse(accepted).unwrap());
assert_eq!(provenance.trust(), TrustClass::Trusted);
}
}
#[test]
fn wire_refuses_locally_only_and_unrecognised_origins() {
for refused in ["user", "schedule", "event", "system", "not-an-origin", ""] {
let provenance = from_wire(Some(refused), Some("trusted"));
assert_eq!(
provenance.origin(),
TaskOrigin::Event,
"{refused:?} must not be accepted from the wire"
);
assert_eq!(provenance.trust(), TrustClass::Untrusted);
}
}
#[test]
fn wire_without_an_origin_header_is_an_untrusted_event() {
let provenance = from_wire(None, Some("trusted"));
assert_eq!(provenance.origin(), TaskOrigin::Event);
assert_eq!(provenance.trust(), TrustClass::Untrusted);
}
#[test]
fn wire_trust_defaults_to_untrusted_when_absent_or_unparseable() {
for trust_header in [None, Some("Trusted"), Some("yes"), Some("")] {
let provenance = from_wire(Some("peer"), trust_header);
assert_eq!(provenance.origin(), TaskOrigin::Peer);
assert_eq!(
provenance.trust(),
TrustClass::Untrusted,
"trust header {trust_header:?} must not resolve to trusted"
);
}
}
#[test]
fn stamp_for_peer_carries_the_senders_class_and_defaults_untrusted() {
assert_eq!(stamp_for_peer(None).origin(), TaskOrigin::Peer);
assert_eq!(stamp_for_peer(None).trust(), TrustClass::Untrusted);
for (sender, expected) in [
(TaskOrigin::User, TrustClass::Trusted),
(TaskOrigin::Event, TrustClass::Untrusted),
] {
let stamped = stamp_for_peer(Some(TaskProvenance::derive(sender, None)));
assert_eq!(stamped.origin(), TaskOrigin::Peer);
assert_eq!(stamped.trust(), expected);
}
}
#[test]
fn stamp_for_completion_carries_the_delegating_tasks_class() {
assert_eq!(stamp_for_completion(None).origin(), TaskOrigin::Completion);
assert_eq!(stamp_for_completion(None).trust(), TrustClass::Untrusted);
for trust in [TrustClass::Trusted, TrustClass::Untrusted] {
let stamped = stamp_for_completion(Some(trust));
assert_eq!(stamped.origin(), TaskOrigin::Completion);
assert_eq!(stamped.trust(), trust);
}
}
#[test]
fn artifact_trust_of_an_operator_pin_is_trusted() {
assert_eq!(
artifact_trust(&murmur_artifact::LockOrigin::Operator),
TrustClass::Trusted
);
}
#[test]
fn artifact_trust_of_a_runtime_pin_is_untrusted_for_any_session() {
for session in ["ses_puller", "", "operator", "trusted"] {
let origin = murmur_artifact::LockOrigin::Runtime {
session: session.to_string(),
};
assert_eq!(
artifact_trust(&origin),
TrustClass::Untrusted,
"session {session:?} must not make a runtime pin trusted"
);
}
}
#[test]
fn wire_untrusted_peer_stays_untrusted() {
let provenance = from_wire(Some("peer"), Some("untrusted"));
assert_eq!(provenance.origin(), TaskOrigin::Peer);
assert_eq!(provenance.trust(), TrustClass::Untrusted);
}
}