//! Tests for the session state files. Do not edit. #[path = "support/tmp.rs"] mod tmp; use brokerd::policy::{Label, SessionState}; use brokerd::state::{RUNBOOK, StateError, StateStore}; use proto::{DataClass, SessionId}; use std::os::unix::fs::PermissionsExt; use tmp::TempDir; fn id(text: &str) -> SessionId { SessionId::new(text).unwrap() } fn label(class: DataClass, untrusted: bool) -> Label { Label { class, untrusted } } fn state(taint: DataClass, untrusted: bool) -> SessionState { SessionState { taint, untrusted } } /// The store's directory is two levels below the temporary one and does not exist yet, as on a /// fresh install. fn store(home: &TempDir) -> StateStore { StateStore::new(&home.path().join("broker/sessions")) } #[test] fn a_session_with_no_file_is_private_and_trusted() { let home = TempDir::new("state"); let store = store(&home); let fresh = store.read(&id("chat-1")).unwrap(); assert_eq!(fresh, state(DataClass::Private, false)); assert_eq!(fresh, SessionState::default()); // Reading creates nothing. assert!(!home.path().join("broker").exists()); } #[test] fn the_first_result_creates_the_file_and_its_directory() { let home = TempDir::new("state"); let store = store(&home); let session = id("chat-1"); let next = store .raise( &session, SessionState::default(), label(DataClass::Private, false), ) .unwrap(); assert_eq!(next, state(DataClass::Private, false)); let path = home.path().join("broker/sessions/chat-1.json"); assert_eq!(store.path(&session), path); assert_eq!( std::fs::read_to_string(&path).unwrap(), "{\"taint\":\"private\",\"untrusted\":false}\n" ); let mode = |p: &std::path::Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777; assert_eq!(mode(&path), 0o600); assert_eq!(mode(&home.path().join("broker/sessions")), 0o700); assert_eq!(mode(&home.path().join("broker")), 0o700); // No temporary file is left behind. assert!(!home.path().join("broker/sessions/chat-1.json.tmp").exists()); assert_eq!(store.read(&session).unwrap(), next); } #[test] fn taint_and_the_untrusted_flag_only_go_up() { let home = TempDir::new("state"); let store = store(&home); let session = id("s"); let steps = [ ( label(DataClass::Public, false), state(DataClass::Private, false), ), ( label(DataClass::Private, true), state(DataClass::Private, true), ), ( label(DataClass::Secret, false), state(DataClass::Secret, true), ), ( label(DataClass::Public, false), state(DataClass::Secret, true), ), ( label(DataClass::Private, false), state(DataClass::Secret, true), ), ]; let mut current = store.read(&session).unwrap(); for (result, want) in steps { current = store.raise(&session, current, result).unwrap(); assert_eq!(current, want); assert_eq!(store.read(&session).unwrap(), want, "what is on disk"); } assert_eq!( std::fs::read_to_string(store.path(&session)).unwrap(), "{\"taint\":\"secret\",\"untrusted\":true}\n" ); } #[test] fn sessions_do_not_share_state() { let home = TempDir::new("state"); let store = store(&home); store .raise( &id("a"), SessionState::default(), label(DataClass::Secret, true), ) .unwrap(); assert_eq!(store.read(&id("b")).unwrap(), SessionState::default()); assert_eq!( store.read(&id("a")).unwrap(), state(DataClass::Secret, true) ); } /// A file that exists but does not hold a valid state is an error, never "no file". #[test] fn a_damaged_file_is_an_error_that_names_the_file_and_the_runbook() { let home = TempDir::new("state"); let store = store(&home); std::fs::create_dir_all(home.path().join("broker/sessions")).unwrap(); let session = id("hurt"); for text in [ "", "{", "null", "[]", "{\"taint\":\"secret\"}", "{\"untrusted\":false}", "{\"taint\":\"internal\",\"untrusted\":false}", "{\"taint\":\"secret\",\"untrusted\":\"no\"}", "{\"taint\":\"secret\",\"untrusted\":false,\"note\":1}", "{\"taint\":\"secret\",\"untrusted\":false} trailing", // A session is never below private, so this file was not written by brokerd. "{\"taint\":\"public\",\"untrusted\":false}", ] { std::fs::write(store.path(&session), text).unwrap(); let err = store.read(&session).expect_err(text); assert!( matches!(err, StateError::Unreadable(..)), "{text:?}: {err:?}" ); let shown = err.to_string(); assert!(shown.contains("hurt.json"), "{shown}"); assert!(shown.ends_with(RUNBOOK), "{shown}"); } assert_eq!(RUNBOOK, "see docs/runbook.md#broker-state-damaged"); // A good file with or without its final newline reads fine. for text in [ "{\"taint\":\"secret\",\"untrusted\":true}\n", "{\"taint\":\"secret\",\"untrusted\":true}", ] { std::fs::write(store.path(&session), text).unwrap(); assert_eq!( store.read(&session).unwrap(), state(DataClass::Secret, true) ); } // Not valid UTF-8, and a directory where the file should be. std::fs::write(store.path(&session), b"\xff\xfe").unwrap(); assert!(matches!( store.read(&session), Err(StateError::Unreadable(..)) )); std::fs::remove_file(store.path(&session)).unwrap(); std::fs::create_dir(store.path(&session)).unwrap(); assert!(matches!( store.read(&session), Err(StateError::Unreadable(..)) )); } #[test] fn a_file_without_read_permission_is_an_error_not_a_fresh_session() { if tmp::running_as_root("a_file_without_read_permission_is_an_error_not_a_fresh_session") { return; } let home = TempDir::new("state"); let store = store(&home); let session = id("locked"); store .raise( &session, SessionState::default(), label(DataClass::Secret, false), ) .unwrap(); let path = store.path(&session); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap(); assert!(matches!( store.read(&session), Err(StateError::Unreadable(..)) )); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap(); } #[test] fn a_failed_write_is_an_error_and_leaves_the_old_state() { if tmp::running_as_root("a_failed_write_is_an_error_and_leaves_the_old_state") { return; } let home = TempDir::new("state"); let store = store(&home); let session = id("s"); let before = store .raise( &session, SessionState::default(), label(DataClass::Private, true), ) .unwrap(); let dir = home.path().join("broker/sessions"); std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o500)).unwrap(); let err = store .raise(&session, before, label(DataClass::Secret, false)) .expect_err("the directory is read-only"); std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap(); assert!(matches!(err, StateError::Write(..)), "{err:?}"); let shown = err.to_string(); assert!(shown.contains("s.json"), "{shown}"); assert!(shown.ends_with(RUNBOOK), "{shown}"); assert_eq!(store.read(&session).unwrap(), before); } /// A `.tmp` file beside the state is a write that did not finish. It is not the state, it does /// not stop the next write, and the next write replaces it. #[test] fn a_leftover_tmp_file_is_neither_read_nor_in_the_way() { let home = TempDir::new("state"); let store = store(&home); let session = id("s"); let dir = home.path().join("broker/sessions"); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("s.json.tmp"), "{\"taint\":\"secret\",\"untr").unwrap(); assert_eq!(store.read(&session).unwrap(), SessionState::default()); let next = store .raise( &session, SessionState::default(), label(DataClass::Secret, false), ) .unwrap(); assert_eq!(store.read(&session).unwrap(), next); assert!(!dir.join("s.json.tmp").exists()); } #[test] fn raise_trusts_the_state_it_is_given_not_the_file() { // The caller read the state under the ledger lock a moment ago; `raise` does not read again. let home = TempDir::new("state"); let store = store(&home); let session = id("s"); let given = state(DataClass::Secret, true); let next = store .raise(&session, given, label(DataClass::Public, false)) .unwrap(); assert_eq!(next, given); // Even a state below private is lifted to private on the way to disk. let low = store .raise( &id("low"), state(DataClass::Public, false), label(DataClass::Public, false), ) .unwrap(); assert_eq!(low, state(DataClass::Private, false)); }