services_hardening_state_host.rs (9418B)
1 #![forbid(unsafe_code)] 2 #![cfg(any(target_os = "linux", target_os = "macos"))] 3 4 use std::{error::Error, fs, os::unix::fs::PermissionsExt, path::Path}; 5 6 use myc::{ 7 MycConfigProfile, MycStateHostErrorKind, MycStateHostMode, MycStateMetadata, 8 RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, initialize_myc_state, 9 open_myc_state_inspection, open_myc_state_read_write, parse_myc_cli_v1_from, 10 parse_myc_config_v1, resolve_myc_runtime_context, 11 }; 12 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; 13 use radroots_storage::event::SourceGeneration; 14 15 const HOST_SOURCE: &str = include_str!("../src/state_host.rs"); 16 const LIB_SOURCE: &str = include_str!("../src/lib.rs"); 17 const CONFIG_EXAMPLE: &[u8] = 18 include_bytes!("../contracts/services_hardening/config.v1.example.toml"); 19 20 fn runtime(root: &Path, instance: &str) -> myc::MycRuntimeContext { 21 let root = root.to_str().expect("UTF-8 temporary root"); 22 let invocation = parse_myc_cli_v1_from([ 23 "myc", 24 "--profile", 25 "repo-local", 26 "--instance", 27 instance, 28 "--repo-local-root", 29 root, 30 "run", 31 ]) 32 .expect("valid test invocation"); 33 resolve_myc_runtime_context( 34 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 35 &invocation, 36 ) 37 .expect("runtime context") 38 } 39 40 fn prepare_state_directory(runtime: &myc::MycRuntimeContext) { 41 let directory = runtime.context().paths().state(); 42 fs::create_dir_all(directory).expect("state directory"); 43 fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).expect("state mode"); 44 } 45 46 fn prepare_state_root(runtime: &myc::MycRuntimeContext) { 47 let root = runtime 48 .context() 49 .paths() 50 .state() 51 .ancestors() 52 .nth(3) 53 .expect("state root"); 54 fs::create_dir_all(root).expect("state root"); 55 fs::set_permissions(root, fs::Permissions::from_mode(0o700)).expect("state root mode"); 56 } 57 58 fn metadata(runtime: &myc::MycRuntimeContext) -> MycStateMetadata { 59 let configuration = 60 parse_myc_config_v1(CONFIG_EXAMPLE, MycConfigProfile::RepoLocal).expect("configuration"); 61 MycStateMetadata::new( 62 runtime, 63 &configuration, 64 SourceGeneration::new([0x5a; 32]).expect("generation"), 65 1_725_000_000_000, 66 ) 67 .expect("metadata") 68 } 69 70 fn migration_evidence() -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) { 71 let applied_at = MigrationAppliedAtUnixSeconds::new(1_725_000_000).expect("migration time"); 72 let build = MigrationBuildIdentity::new( 73 env!("CARGO_PKG_VERSION"), 74 "1111111111111111111111111111111111111111", 75 "053d0c750bf9cd683c6ea37cefe7e79617ba629f", 76 "rustc-test", 77 "test-target", 78 "service-host", 79 1, 80 myc::MYC_STATE_SCHEMA_VERSION, 81 1, 82 1, 83 1, 84 ) 85 .expect("build identity"); 86 (applied_at, build) 87 } 88 89 fn mismatched_migration_build() -> MigrationBuildIdentity { 90 MigrationBuildIdentity::new( 91 env!("CARGO_PKG_VERSION"), 92 "1111111111111111111111111111111111111111", 93 "053d0c750bf9cd683c6ea37cefe7e79617ba629f", 94 "rustc-test", 95 "test-target", 96 "service-host", 97 1, 98 1, 99 1, 100 1, 101 1, 102 ) 103 .expect("structurally valid mismatched build") 104 } 105 106 #[tokio::test] 107 async fn initialize_is_create_new_and_both_existing_open_modes_close_explicitly() { 108 let directory = tempfile::tempdir().expect("temporary root"); 109 let runtime = runtime(directory.path(), "primary"); 110 prepare_state_root(&runtime); 111 let metadata = metadata(&runtime); 112 let state = runtime.artifacts().state_database(); 113 let lock = runtime.artifacts().state_lock(); 114 115 assert!(!runtime.context().paths().state().exists()); 116 assert!(!state.exists()); 117 let (applied_at, build) = migration_evidence(); 118 initialize_myc_state(&runtime, &metadata, applied_at, &build) 119 .await 120 .expect("create-new initialization"); 121 assert!(state.is_file()); 122 assert!(lock.is_file()); 123 assert_eq!( 124 fs::metadata(state).unwrap().permissions().mode() & 0o777, 125 0o600 126 ); 127 assert_eq!( 128 fs::metadata(lock).unwrap().permissions().mode() & 0o777, 129 0o600 130 ); 131 132 let duplicate = initialize_myc_state(&runtime, &metadata, applied_at, &build) 133 .await 134 .expect_err("second initialization must fail"); 135 assert_eq!(duplicate.kind(), MycStateHostErrorKind::Initialize); 136 137 let writer = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 138 .await 139 .expect("existing writable state"); 140 assert_eq!(writer.mode(), MycStateHostMode::ReadWriteExisting); 141 assert_eq!( 142 format!("{writer:?}"), 143 "MycStateHost { mode: ReadWriteExisting, state: \"[sealed]\" }" 144 ); 145 146 let contended = open_myc_state_inspection(&runtime, &metadata) 147 .await 148 .expect_err("inspection must not bypass active writer authority"); 149 assert_eq!(contended.kind(), MycStateHostErrorKind::InspectionOpen); 150 writer.close().await.expect("writer close"); 151 writer.close().await.expect("idempotent writer close"); 152 153 let inspection = open_myc_state_inspection(&runtime, &metadata) 154 .await 155 .expect("existing inspection state"); 156 assert_eq!(inspection.mode(), MycStateHostMode::ReadOnlyInspection); 157 inspection.close().await.expect("inspection close"); 158 159 let writer = open_myc_state_read_write(&runtime, &metadata, applied_at, &build) 160 .await 161 .expect("authority reacquisition after explicit close"); 162 writer.close().await.expect("reopened writer close"); 163 } 164 165 #[tokio::test] 166 async fn missing_state_and_mismatched_evidence_fail_before_database_creation() { 167 let invalid_directory = tempfile::tempdir().expect("invalid temporary root"); 168 let invalid_runtime = runtime(invalid_directory.path(), "invalid"); 169 let invalid_metadata = metadata(&invalid_runtime); 170 let (applied_at, build) = migration_evidence(); 171 let invalid_build = initialize_myc_state( 172 &invalid_runtime, 173 &invalid_metadata, 174 applied_at, 175 &mismatched_migration_build(), 176 ) 177 .await 178 .expect_err("migration build contract mismatch"); 179 assert_eq!(invalid_build.kind(), MycStateHostErrorKind::InvalidEvidence); 180 assert!(!invalid_runtime.context().paths().state().exists()); 181 182 let missing_directory = tempfile::tempdir().expect("missing temporary root"); 183 let missing_runtime = runtime(missing_directory.path(), "missing"); 184 prepare_state_root(&missing_runtime); 185 let missing_metadata = metadata(&missing_runtime); 186 let missing = 187 open_myc_state_read_write(&missing_runtime, &missing_metadata, applied_at, &build) 188 .await 189 .expect_err("existing-only open never provisions the service suffix"); 190 assert_eq!(missing.kind(), MycStateHostErrorKind::ReadWriteOpen); 191 assert!(!missing_runtime.context().paths().state().exists()); 192 193 let directory = tempfile::tempdir().expect("temporary root"); 194 let primary = runtime(directory.path(), "primary"); 195 let secondary = runtime(directory.path(), "secondary"); 196 prepare_state_directory(&primary); 197 let primary_metadata = metadata(&primary); 198 199 let missing = open_myc_state_read_write(&primary, &primary_metadata, applied_at, &build) 200 .await 201 .expect_err("missing state is never created by open"); 202 assert_eq!(missing.kind(), MycStateHostErrorKind::ReadWriteOpen); 203 assert!(!primary.artifacts().state_database().exists()); 204 205 let mismatch = initialize_myc_state(&secondary, &primary_metadata, applied_at, &build) 206 .await 207 .expect_err("cross-instance metadata"); 208 assert_eq!(mismatch.kind(), MycStateHostErrorKind::InvalidEvidence); 209 assert_eq!(mismatch.code(), "state_evidence_invalid"); 210 assert!(Error::source(&mismatch).is_none()); 211 let rendered = format!("{mismatch} {mismatch:?}"); 212 assert!(!rendered.contains(directory.path().to_string_lossy().as_ref())); 213 assert!(!rendered.contains("state.sqlite")); 214 assert!(!secondary.artifacts().state_database().exists()); 215 } 216 217 #[test] 218 fn public_lifecycle_source_is_sealed() { 219 assert!(LIB_SOURCE.contains("mod state_host;")); 220 assert!(!LIB_SOURCE.contains("pub mod state_host;")); 221 assert!(HOST_SOURCE.contains("host: ServiceSqliteHost")); 222 assert!(!HOST_SOURCE.contains("pub host:")); 223 for required in [ 224 "ServiceSqliteInitializer", 225 "ServiceSqliteInitializerFuture", 226 ".state_directory_plan()", 227 ".and_then(|plan| plan.provision())", 228 ] { 229 assert!( 230 HOST_SOURCE.contains(required), 231 "missing sealed initialization boundary `{required}`" 232 ); 233 } 234 for forbidden in [ 235 "pub fn transaction", 236 "pub async fn transaction", 237 "pub fn pool", 238 "pub fn connection", 239 "pub fn into_inner", 240 "pub fn executor", 241 "MigrationDescriptor::", 242 "raw_sql", 243 "CREATE TABLE", 244 "PRAGMA application_id", 245 "PathBuf", 246 "use sqlx::", 247 "SqliteConnectOptions", 248 "ConnectOptions", 249 "create_dir_all", 250 "try_exists", 251 ] { 252 assert!( 253 !HOST_SOURCE.contains(forbidden), 254 "found forbidden lifecycle authority `{forbidden}`" 255 ); 256 } 257 }