186 lines
7.7 KiB
Rust
186 lines
7.7 KiB
Rust
// file: crates/ksp-worker-api/tests/security_hardening.rs
|
|
// version: 1
|
|
|
|
//! Adversarial lifecycle, stop and redaction canaries for the frozen Worker API.
|
|
|
|
const HOSTILE_MARKER: &str = "WORKER-IDENTITY-SECRET-CANARY";
|
|
const TEST_FAULT: ksp_worker_api::ErrorCode = ksp_worker_api::ErrorCode::new("worker_test", "fault");
|
|
|
|
fn running_lifecycle(id_value: &str) -> std::option::Option<ksp_worker_api::WorkerLifecycle> {
|
|
let id = match ksp_worker_api::WorkerId::new(id_value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let kind = match ksp_worker_api::WorkerKindCode::new("continuous_worker") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return std::option::Option::None,
|
|
};
|
|
let mut lifecycle = ksp_worker_api::WorkerLifecycle::new(id, kind);
|
|
if lifecycle.start().is_err() {
|
|
return std::option::Option::None;
|
|
}
|
|
if lifecycle.mark_running().is_err() {
|
|
return std::option::Option::None;
|
|
}
|
|
return std::option::Option::Some(lifecycle);
|
|
}
|
|
|
|
fn assert_all_mutators_reject(lifecycle: &mut ksp_worker_api::WorkerLifecycle, terminal: ksp_worker_api::WorkerState) {
|
|
assert!(lifecycle.start().is_err());
|
|
assert!(lifecycle.mark_running().is_err());
|
|
assert!(lifecycle.mark_stopping().is_err());
|
|
assert!(lifecycle.mark_stopped().is_err());
|
|
assert!(lifecycle.fault(TEST_FAULT).is_err());
|
|
assert_eq!(lifecycle.state(), terminal);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_worker_identity_lifecycle_and_snapshot_debug_redact_hostile_identity() {
|
|
let lifecycle = running_lifecycle(HOSTILE_MARKER);
|
|
assert!(lifecycle.is_some());
|
|
let lifecycle = match lifecycle {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let id = lifecycle.id();
|
|
assert_eq!(std::format!("{id:?}"), "WorkerId(..)");
|
|
let lifecycle_debug = std::format!("{lifecycle:?}");
|
|
assert!(lifecycle_debug.contains("WorkerLifecycle"));
|
|
assert!(lifecycle_debug.contains("continuous_worker"));
|
|
assert!(lifecycle_debug.contains("Running"));
|
|
assert!(!lifecycle_debug.contains(HOSTILE_MARKER));
|
|
let snapshot = ksp_worker_api::WorkerSnapshot::new(
|
|
lifecycle.id().clone(),
|
|
lifecycle.kind().clone(),
|
|
ksp_worker_api::WorkerSnapshotSequence::initial(),
|
|
lifecycle.state(),
|
|
ksp_worker_api::WorkerHealth::Healthy,
|
|
ksp_worker_api::WorkerActivity::Active,
|
|
);
|
|
let snapshot_debug = std::format!("{snapshot:?}");
|
|
assert!(snapshot_debug.contains("WorkerSnapshot"));
|
|
assert!(snapshot_debug.contains("continuous_worker"));
|
|
assert!(snapshot_debug.contains("Healthy"));
|
|
assert!(snapshot_debug.contains("Active"));
|
|
assert!(!snapshot_debug.contains(HOSTILE_MARKER));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_identity_errors_never_echo_hostile_values() {
|
|
let hostile = "WORKER-SECRET/CANARY";
|
|
let rejected = ksp_worker_api::WorkerId::new(hostile);
|
|
assert!(rejected.is_err());
|
|
let error = match rejected {
|
|
std::result::Result::Err(value) => value,
|
|
std::result::Result::Ok(_) => return,
|
|
};
|
|
assert_eq!(error.code(), ksp_worker_api::ERROR_CODE_WORKER_ID_INVALID);
|
|
assert!(!std::format!("{error}").contains(hostile));
|
|
assert!(!std::format!("{error:?}").contains(hostile));
|
|
for context in error.context() {
|
|
assert!(!context.value().contains(hostile));
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_stopped_and_faulted_are_immutable_under_all_public_mutators() {
|
|
let id = match ksp_worker_api::WorkerId::new("terminal-created") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let kind = match ksp_worker_api::WorkerKindCode::new("terminal_worker") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
let mut stopped = ksp_worker_api::WorkerLifecycle::new(id, kind);
|
|
assert!(stopped.mark_stopped().is_ok());
|
|
assert_all_mutators_reject(&mut stopped, ksp_worker_api::WorkerState::Stopped);
|
|
let faulted = running_lifecycle("terminal-faulted");
|
|
assert!(faulted.is_some());
|
|
let mut faulted = match faulted {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(faulted.fault(TEST_FAULT).is_ok());
|
|
assert_all_mutators_reject(&mut faulted, ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_stop_fault_terminal_orders_are_first_valid_terminal_wins() {
|
|
let direct_fault = running_lifecycle("race-direct-fault");
|
|
assert!(direct_fault.is_some());
|
|
let mut direct_fault = match direct_fault {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(direct_fault.fault(TEST_FAULT).is_ok());
|
|
assert!(direct_fault.mark_stopping().is_err());
|
|
assert_eq!(direct_fault.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
|
let stopping_fault = running_lifecycle("race-stopping-fault");
|
|
assert!(stopping_fault.is_some());
|
|
let mut stopping_fault = match stopping_fault {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(stopping_fault.mark_stopping().is_ok());
|
|
assert!(stopping_fault.fault(TEST_FAULT).is_ok());
|
|
assert!(stopping_fault.mark_stopped().is_err());
|
|
assert_eq!(stopping_fault.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
|
let stopping_stopped = running_lifecycle("race-stopping-stopped");
|
|
assert!(stopping_stopped.is_some());
|
|
let mut stopping_stopped = match stopping_stopped {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(stopping_stopped.mark_stopping().is_ok());
|
|
assert!(stopping_stopped.mark_stopped().is_ok());
|
|
assert!(stopping_stopped.fault(TEST_FAULT).is_err());
|
|
assert_eq!(stopping_stopped.state(), ksp_worker_api::WorkerState::Stopped);
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_stop_token_is_send_sync_cross_thread_and_cannot_reanimate_terminal_lifecycle() {
|
|
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
|
|
require_send_sync::<ksp_worker_api::WorkerStopToken>();
|
|
let lifecycle = running_lifecycle("cross-thread-stop");
|
|
assert!(lifecycle.is_some());
|
|
let mut lifecycle = match lifecycle {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
assert!(lifecycle.fault(TEST_FAULT).is_ok());
|
|
let token = ksp_worker_api::WorkerStopToken::new();
|
|
let worker_token = token.clone();
|
|
let thread = std::thread::spawn(move || return worker_token.request_stop());
|
|
let first = match thread.join() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => return,
|
|
};
|
|
assert!(first);
|
|
assert!(token.is_stop_requested());
|
|
assert!(!token.request_stop());
|
|
assert!(lifecycle.mark_stopping().is_err());
|
|
assert_eq!(lifecycle.state(), ksp_worker_api::WorkerState::Faulted(TEST_FAULT));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_public_worker_primitives_keep_promised_send_sync_contracts() {
|
|
fn require_send_sync<T: std::marker::Send + std::marker::Sync>() {}
|
|
require_send_sync::<ksp_worker_api::WorkerId>();
|
|
require_send_sync::<ksp_worker_api::WorkerKindCode>();
|
|
require_send_sync::<ksp_worker_api::WorkerState>();
|
|
require_send_sync::<ksp_worker_api::WorkerHealth>();
|
|
require_send_sync::<ksp_worker_api::WorkerActivity>();
|
|
require_send_sync::<ksp_worker_api::WorkerLifecycle>();
|
|
require_send_sync::<ksp_worker_api::WorkerSnapshotSequence>();
|
|
require_send_sync::<ksp_worker_api::WorkerSnapshot>();
|
|
require_send_sync::<ksp_worker_api::WorkerStopToken>();
|
|
return;
|
|
}
|