services_hardening_presence_desired_state.rs (11973B)
1 #![forbid(unsafe_code)] 2 #![cfg(any(target_os = "linux", target_os = "macos"))] 3 4 use std::{fs, os::unix::fs::PermissionsExt, path::Path}; 5 6 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity}; 7 use radroots_storage::event::SourceGeneration; 8 use rhi::{ 9 RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiConfigDocumentV1, 10 RhiConfigProfile, RhiPresenceDesiredAuthority, RhiPresenceDesiredErrorKind, 11 RhiPresenceDesiredMode, RhiStateMetadata, apply_rhi_configuration, initialize_rhi_state, 12 open_rhi_state_inspection, open_rhi_state_read_write_from_config, parse_rhi_cli_v1_from, 13 parse_rhi_config_v1, resolve_rhi_runtime_context, validate_rhi_presence_desired_authority, 14 }; 15 use sqlx::{ConnectOptions, Connection, Row, SqliteConnection, sqlite::SqliteConnectOptions}; 16 17 const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 18 const CONTRACT: &str = 19 include_str!("../contracts/services_hardening/presence_desired_state.v1.json"); 20 const SOURCE: &str = include_str!("../src/presence_desired.rs"); 21 22 fn runtime(root: &Path) -> rhi::RhiRuntimeContext { 23 let invocation = parse_rhi_cli_v1_from([ 24 "rhi", 25 "--profile", 26 "repo-local", 27 "--instance", 28 "primary", 29 "--repo-local-root", 30 root.to_str().expect("UTF-8 root"), 31 "run", 32 ]) 33 .expect("invocation"); 34 resolve_rhi_runtime_context( 35 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 36 &invocation, 37 ) 38 .expect("runtime") 39 } 40 41 fn config(source: &str) -> RhiConfigDocumentV1 { 42 parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal).expect("configuration") 43 } 44 45 fn evidence(at: u64) -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) { 46 let applied_at = MigrationAppliedAtUnixSeconds::new(at).expect("migration time"); 47 let build = MigrationBuildIdentity::new( 48 env!("CARGO_PKG_VERSION"), 49 "1111111111111111111111111111111111111111", 50 "053d0c750bf9cd683c6ea37cefe7e79617ba629f", 51 "rustc-test", 52 "test-target", 53 "service-host", 54 1, 55 rhi::RHI_STATE_SCHEMA_VERSION, 56 1, 57 1, 58 1, 59 ) 60 .expect("build identity"); 61 (applied_at, build) 62 } 63 64 async fn offline_connection(runtime: &rhi::RhiRuntimeContext) -> SqliteConnection { 65 let options = SqliteConnectOptions::new() 66 .filename(runtime.artifacts().state_database()) 67 .create_if_missing(false) 68 .foreign_keys(false) 69 .disable_statement_logging(); 70 SqliteConnection::connect_with(&options) 71 .await 72 .expect("offline connection") 73 } 74 75 #[tokio::test] 76 async fn desired_state_is_durable_exact_replay_and_semantic_change_only() { 77 let directory = tempfile::tempdir().expect("root"); 78 let runtime = runtime(directory.path()); 79 fs::create_dir_all(runtime.context().paths().state()).expect("state directory"); 80 fs::set_permissions( 81 runtime.context().paths().state(), 82 fs::Permissions::from_mode(0o700), 83 ) 84 .expect("state mode"); 85 let original = config(EXAMPLE); 86 let metadata = RhiStateMetadata::new( 87 &runtime, 88 &original, 89 SourceGeneration::new([0x5a; 32]).expect("source generation"), 90 1_725_000_000_000, 91 ) 92 .expect("metadata"); 93 let (first_at, first_build) = evidence(1_725_000_000); 94 initialize_rhi_state(&runtime, &metadata, first_at, &first_build) 95 .await 96 .expect("initialize"); 97 98 let original_authority = 99 RhiPresenceDesiredAuthority::from_config(&original).expect("authority"); 100 let host = open_rhi_state_read_write_from_config(&runtime, &original, first_at, &first_build) 101 .await 102 .expect("writer"); 103 let repository = host.repositories().desired_presence(); 104 assert_eq!(repository.current().await.expect("initial read"), None); 105 let first = repository 106 .commit(&original_authority) 107 .await 108 .expect("first commit"); 109 assert!(first.changed()); 110 assert_eq!(first.state().generation(), 1); 111 assert_eq!(first.state().mode(), RhiPresenceDesiredMode::Enabled); 112 assert!(first.state().profile()); 113 assert!(first.state().application_handler()); 114 assert_eq!(first.state().target_count(), 2); 115 assert_eq!(first.state().required_target_count(), 1); 116 assert_eq!(first.state().queue_capacity(), 64); 117 let replay = repository 118 .commit(&original_authority) 119 .await 120 .expect("exact replay"); 121 assert!(!replay.changed()); 122 assert_eq!(replay.state(), first.state()); 123 assert_eq!( 124 repository.current().await.expect("current"), 125 Some(first.state()) 126 ); 127 host.close().await.expect("writer close"); 128 129 let inspection = open_rhi_state_inspection(&runtime, &metadata) 130 .await 131 .expect("inspection"); 132 let inspected = inspection 133 .repositories() 134 .desired_presence() 135 .current() 136 .await 137 .expect("inspection read"); 138 assert_eq!(inspected, Some(first.state())); 139 let rejected = inspection 140 .repositories() 141 .desired_presence() 142 .commit(&original_authority) 143 .await 144 .expect_err("inspection mutation"); 145 assert_eq!(rejected.kind(), RhiPresenceDesiredErrorKind::InvalidMode); 146 inspection.close().await.expect("inspection close"); 147 148 let unrelated_source = EXAMPLE.replacen("level = \"info\"", "level = \"debug\"", 1); 149 let unrelated = config(&unrelated_source); 150 let unrelated_authority = 151 RhiPresenceDesiredAuthority::from_config(&unrelated).expect("unrelated authority"); 152 assert_eq!( 153 unrelated_authority.desired_sha256(), 154 original_authority.desired_sha256() 155 ); 156 assert_eq!( 157 validate_rhi_presence_desired_authority(&unrelated, &original_authority) 158 .expect_err("full configuration binding") 159 .kind(), 160 RhiPresenceDesiredErrorKind::Binding 161 ); 162 let (second_at, second_build) = evidence(1_725_000_001); 163 apply_rhi_configuration(&runtime, &original, &unrelated, second_at, &second_build) 164 .await 165 .expect("apply unrelated configuration"); 166 let host = 167 open_rhi_state_read_write_from_config(&runtime, &unrelated, second_at, &second_build) 168 .await 169 .expect("writer after unrelated config"); 170 let replay = host 171 .repositories() 172 .desired_presence() 173 .commit(&unrelated_authority) 174 .await 175 .expect("semantic replay"); 176 assert!(!replay.changed()); 177 assert_eq!(replay.state().generation(), 1); 178 host.close().await.expect("writer close"); 179 180 let presence_source = unrelated_source.replace("profile = true", "profile = false"); 181 let presence_changed = config(&presence_source); 182 let changed_authority = 183 RhiPresenceDesiredAuthority::from_config(&presence_changed).expect("changed authority"); 184 assert_ne!( 185 changed_authority.desired_sha256(), 186 original_authority.desired_sha256() 187 ); 188 let (third_at, third_build) = evidence(1_725_000_002); 189 apply_rhi_configuration( 190 &runtime, 191 &unrelated, 192 &presence_changed, 193 third_at, 194 &third_build, 195 ) 196 .await 197 .expect("apply presence configuration"); 198 let host = 199 open_rhi_state_read_write_from_config(&runtime, &presence_changed, third_at, &third_build) 200 .await 201 .expect("writer after presence change"); 202 let stale = host 203 .repositories() 204 .desired_presence() 205 .commit(&original_authority) 206 .await 207 .expect_err("stale config authority"); 208 assert_eq!(stale.kind(), RhiPresenceDesiredErrorKind::Binding); 209 let changed = host 210 .repositories() 211 .desired_presence() 212 .commit(&changed_authority) 213 .await 214 .expect("changed desired state"); 215 assert!(changed.changed()); 216 assert_eq!(changed.state().generation(), 2); 217 assert!(!changed.state().profile()); 218 assert!(changed.state().application_handler()); 219 assert_eq!(changed.state().target_count(), 2); 220 assert_eq!( 221 changed.state().desired_sha256(), 222 changed_authority.desired_sha256() 223 ); 224 host.close().await.expect("final writer close"); 225 226 let mut connection = offline_connection(&runtime).await; 227 let row = sqlx::query( 228 "SELECT COUNT(*) AS row_count, generation, length(desired_sha256) AS digest_bytes \ 229 FROM presence_desired_state", 230 ) 231 .fetch_one(&mut connection) 232 .await 233 .expect("durable desired state"); 234 assert_eq!(row.try_get::<i64, _>("row_count").unwrap(), 1); 235 assert_eq!(row.try_get::<i64, _>("generation").unwrap(), 2); 236 assert_eq!(row.try_get::<i64, _>("digest_bytes").unwrap(), 32); 237 assert!( 238 sqlx::query("UPDATE presence_desired_state SET generation = generation") 239 .execute(&mut connection) 240 .await 241 .is_err() 242 ); 243 assert!( 244 sqlx::query("DELETE FROM presence_desired_state") 245 .execute(&mut connection) 246 .await 247 .is_err() 248 ); 249 connection.close().await.expect("offline close"); 250 251 let database = fs::read(runtime.artifacts().state_database()).expect("database bytes"); 252 for forbidden in [ 253 b"wss://relay-primary.example".as_slice(), 254 b"relay-primary".as_slice(), 255 directory.path().to_string_lossy().as_bytes(), 256 ] { 257 assert!( 258 !database 259 .windows(forbidden.len()) 260 .any(|window| window == forbidden) 261 ); 262 } 263 } 264 265 #[test] 266 fn machine_contract_freezes_desired_state_without_publication_effects() { 267 let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract"); 268 assert_eq!(contract["schema"], "radroots.rhi.presence-desired-state"); 269 assert_eq!(contract["contract_version"], 1); 270 assert_eq!(contract["authority"]["maximum_targets"], 32); 271 assert_eq!( 272 contract["reference_vector"]["target_set_sha256"], 273 "959f04012841ae6e9bf3e109468b4f66cfa9d966aac1df36df09e45c1e1c48f9" 274 ); 275 assert_eq!( 276 contract["reference_vector"]["desired_state_sha256"], 277 "7235f1e386e839427625dc364df7b51ee74d39d5f170e12b25cf2c42fd7731f0" 278 ); 279 assert_eq!(contract["effects"]["clock"], false); 280 assert_eq!(contract["effects"]["entropy"], false); 281 assert_eq!(contract["effects"]["network"], false); 282 assert_eq!(contract["effects"]["relay_io"], false); 283 assert_eq!( 284 contract["separate_publication_authority"] 285 .as_array() 286 .unwrap() 287 .len(), 288 7 289 ); 290 for required in [ 291 "DESIRED_STATE_DOMAIN", 292 "TARGET_SET_DOMAIN", 293 "pub fn validate_rhi_presence_desired_authority(", 294 "pub async fn commit(", 295 "pub async fn current(", 296 "require_current_config(transaction, authority).await?", 297 "LIMIT 2", 298 "ServiceSqliteTransactionErrorKind::CommitOutcomeUnknown", 299 ] { 300 assert!( 301 SOURCE.contains(required), 302 "missing Step204 boundary {required}" 303 ); 304 } 305 for forbidden in [ 306 "SystemTime", 307 "OsRng", 308 "thread_rng", 309 "tokio::spawn", 310 "std::net", 311 "NostrEventAdapter", 312 "sign_nostr_event", 313 ] { 314 assert!( 315 !SOURCE.contains(forbidden), 316 "unexpected desired-state effect {forbidden}" 317 ); 318 } 319 for kind in [ 320 RhiPresenceDesiredErrorKind::InvalidConfiguration, 321 RhiPresenceDesiredErrorKind::TargetInventory, 322 RhiPresenceDesiredErrorKind::InvalidMode, 323 RhiPresenceDesiredErrorKind::Binding, 324 RhiPresenceDesiredErrorKind::ResourceExhausted, 325 RhiPresenceDesiredErrorKind::Storage, 326 RhiPresenceDesiredErrorKind::CommitOutcomeUnknown, 327 ] { 328 let rendered = format!("{kind:?}"); 329 assert!(!rendered.contains("relay-primary")); 330 } 331 }