services_hardening_runtime_context.rs (10134B)
1 #![forbid(unsafe_code)] 2 3 use std::error::Error; 4 use std::path::{Path, PathBuf}; 5 6 use rhi::{ 7 RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, 8 RhiBootstrapProfileV1, RuntimeContextSource, parse_rhi_cli_v1_from, 9 resolve_rhi_runtime_context, 10 }; 11 12 fn resolve( 13 resolver: &RadrootsPathResolver, 14 profile: &str, 15 instance: &str, 16 repo_local_root: Option<&str>, 17 config_path: Option<&str>, 18 ) -> rhi::RhiRuntimeContext { 19 let mut arguments = vec!["rhi", "--profile", profile, "--instance", instance]; 20 if let Some(root) = repo_local_root { 21 arguments.extend(["--repo-local-root", root]); 22 } 23 if let Some(path) = config_path { 24 arguments.extend(["--config", path]); 25 } 26 arguments.push("run"); 27 let invocation = parse_rhi_cli_v1_from(arguments).expect("validated invocation"); 28 resolve_rhi_runtime_context(resolver, &invocation).expect("runtime context") 29 } 30 31 fn assert_roots(context: &rhi::RhiRuntimeContext, expected: [&str; 6]) { 32 let paths = context.context().paths(); 33 assert_eq!(paths.config(), Path::new(expected[0])); 34 assert_eq!(paths.state(), Path::new(expected[1])); 35 assert_eq!(paths.cache(), Path::new(expected[2])); 36 assert_eq!(paths.logs(), Path::new(expected[3])); 37 assert_eq!(paths.run(), Path::new(expected[4])); 38 assert_eq!(paths.secrets(), Path::new(expected[5])); 39 } 40 41 #[test] 42 fn repo_local_context_binds_identity_provenance_and_exact_artifacts() { 43 let resolver = 44 RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); 45 let primary = resolve( 46 &resolver, 47 "repo-local", 48 "primary", 49 Some("/repo/.local/radroots"), 50 None, 51 ); 52 let secondary = resolve( 53 &resolver, 54 "repo-local", 55 "secondary", 56 Some("/repo/.local/radroots"), 57 None, 58 ); 59 60 assert_eq!(primary.context().service().as_str(), "rhi"); 61 assert_eq!(primary.context().instance().as_str(), "primary"); 62 assert_eq!(primary.profile(), RhiBootstrapProfileV1::RepoLocal); 63 assert_eq!(primary.context().profile(), RadrootsPathProfile::RepoLocal); 64 assert_eq!( 65 primary.context().sources().service(), 66 RuntimeContextSource::SafeDefault 67 ); 68 assert_eq!( 69 primary.context().sources().instance(), 70 RuntimeContextSource::BootstrapCli 71 ); 72 assert_eq!( 73 primary.context().sources().profile(), 74 RuntimeContextSource::BootstrapCli 75 ); 76 assert_eq!( 77 primary.context().sources().repo_local_root(), 78 Some(RuntimeContextSource::BootstrapCli) 79 ); 80 assert_eq!( 81 primary.context().sources().paths(), 82 RuntimeContextSource::DerivedPath 83 ); 84 assert_roots( 85 &primary, 86 [ 87 "/repo/.local/radroots/config/services/rhi/primary", 88 "/repo/.local/radroots/data/services/rhi/primary", 89 "/repo/.local/radroots/cache/services/rhi/primary", 90 "/repo/.local/radroots/logs/services/rhi/primary", 91 "/repo/.local/radroots/run/services/rhi/primary", 92 "/repo/.local/radroots/secrets/services/rhi/primary", 93 ], 94 ); 95 assert_eq!( 96 primary.artifacts().config(), 97 Path::new("/repo/.local/radroots/config/services/rhi/primary/config.toml") 98 ); 99 assert_eq!( 100 primary.artifacts().state_database(), 101 Path::new("/repo/.local/radroots/data/services/rhi/primary/state.sqlite") 102 ); 103 assert_eq!( 104 primary.artifacts().state_lock(), 105 Path::new("/repo/.local/radroots/data/services/rhi/primary/state.lock") 106 ); 107 assert_eq!( 108 primary.artifacts().admin_socket(), 109 Path::new("/repo/.local/radroots/run/services/rhi/primary/admin.sock") 110 ); 111 assert_eq!( 112 primary.identity_path(), 113 Path::new("/repo/.local/radroots/secrets/services/rhi/primary/service.identity.ncrypt") 114 ); 115 assert_eq!(primary.selected_config_path(), primary.artifacts().config()); 116 assert_ne!(primary.context().paths(), secondary.context().paths()); 117 } 118 119 #[test] 120 fn service_host_and_interactive_profiles_have_exact_roots() { 121 let service_host = resolve( 122 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 123 "service-host", 124 "default", 125 None, 126 None, 127 ); 128 assert_roots( 129 &service_host, 130 [ 131 "/etc/radroots/services/rhi/default", 132 "/var/lib/radroots/services/rhi/default", 133 "/var/cache/radroots/services/rhi/default", 134 "/var/log/radroots/services/rhi/default", 135 "/run/radroots/services/rhi/default", 136 "/etc/radroots/secrets/services/rhi/default", 137 ], 138 ); 139 140 let interactive = resolve( 141 &RadrootsPathResolver::new( 142 RadrootsPlatform::Linux, 143 RadrootsHostEnvironment { 144 home_dir: Some(PathBuf::from("/home/operator")), 145 xdg_config_home: Some(PathBuf::from("/xdg/config")), 146 xdg_data_home: Some(PathBuf::from("/xdg/data")), 147 xdg_state_home: Some(PathBuf::from("/xdg/state")), 148 xdg_cache_home: Some(PathBuf::from("/xdg/cache")), 149 xdg_runtime_dir: Some(PathBuf::from("/xdg/run")), 150 ..RadrootsHostEnvironment::default() 151 }, 152 ), 153 "interactive", 154 "default", 155 None, 156 Some("/operator/rhi.toml"), 157 ); 158 assert_roots( 159 &interactive, 160 [ 161 "/xdg/config/radroots/services/rhi/default", 162 "/xdg/data/radroots/services/rhi/default", 163 "/xdg/cache/radroots/services/rhi/default", 164 "/xdg/state/radroots/logs/services/rhi/default", 165 "/xdg/run/radroots/services/rhi/default", 166 "/xdg/config/radroots/secrets/services/rhi/default", 167 ], 168 ); 169 assert_eq!( 170 interactive.selected_config_path(), 171 Path::new("/operator/rhi.toml") 172 ); 173 } 174 175 #[test] 176 fn macos_and_windows_interactive_roots_remain_exact_and_injected() { 177 let macos = resolve( 178 &RadrootsPathResolver::new( 179 RadrootsPlatform::Macos, 180 RadrootsHostEnvironment { 181 home_dir: Some(PathBuf::from("/Users/operator")), 182 ..RadrootsHostEnvironment::default() 183 }, 184 ), 185 "interactive", 186 "default", 187 None, 188 None, 189 ); 190 assert_roots( 191 &macos, 192 [ 193 "/Users/operator/Library/Application Support/Radroots/config/services/rhi/default", 194 "/Users/operator/Library/Application Support/Radroots/data/services/rhi/default", 195 "/Users/operator/Library/Caches/Radroots/services/rhi/default", 196 "/Users/operator/Library/Logs/Radroots/services/rhi/default", 197 "/Users/operator/Library/Application Support/Radroots/run/services/rhi/default", 198 "/Users/operator/Library/Application Support/Radroots/secrets/services/rhi/default", 199 ], 200 ); 201 202 let windows = resolve( 203 &RadrootsPathResolver::new( 204 RadrootsPlatform::Windows, 205 RadrootsHostEnvironment { 206 appdata_dir: Some(PathBuf::from(r"C:\Users\operator\AppData\Roaming")), 207 localappdata_dir: Some(PathBuf::from(r"C:\Users\operator\AppData\Local")), 208 ..RadrootsHostEnvironment::default() 209 }, 210 ), 211 "interactive", 212 "default", 213 None, 214 None, 215 ); 216 assert_roots( 217 &windows, 218 [ 219 r"C:\Users\operator\AppData\Roaming/Radroots/config/services/rhi/default", 220 r"C:\Users\operator\AppData\Local/Radroots/data/services/rhi/default", 221 r"C:\Users\operator\AppData\Local/Radroots/cache/services/rhi/default", 222 r"C:\Users\operator\AppData\Local/Radroots/logs/services/rhi/default", 223 r"C:\Users\operator\AppData\Local/Radroots/run/services/rhi/default", 224 r"C:\Users\operator\AppData\Roaming/Radroots/secrets/services/rhi/default", 225 ], 226 ); 227 } 228 229 #[test] 230 fn debug_and_errors_do_not_disclose_paths_or_instances() { 231 let context = resolve( 232 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 233 "repo-local", 234 "secret-instance", 235 Some("/secret/root"), 236 Some("/secret/config.toml"), 237 ); 238 let rendered = format!("{context:?}"); 239 for forbidden in ["secret-instance", "/secret/root", "/secret/config.toml"] { 240 assert!(!rendered.contains(forbidden), "{rendered}"); 241 } 242 243 let invocation = parse_rhi_cli_v1_from([ 244 "rhi", 245 "--profile", 246 "interactive", 247 "--instance", 248 "default", 249 "run", 250 ]) 251 .expect("invocation"); 252 let error = resolve_rhi_runtime_context( 253 &RadrootsPathResolver::new(RadrootsPlatform::Macos, RadrootsHostEnvironment::default()), 254 &invocation, 255 ) 256 .expect_err("missing HOME"); 257 assert!(error.source().is_none()); 258 assert!(!format!("{error:?} {error}").contains("HOME")); 259 } 260 261 #[test] 262 fn source_contains_no_legacy_path_authority() { 263 let lib = include_str!("../src/lib.rs"); 264 let context = include_str!("../src/runtime_context.rs"); 265 let main = include_str!("../src/main.rs"); 266 for forbidden in [ 267 "pub mod host_paths", 268 "pub mod paths", 269 "RHI_PATHS_PROFILE", 270 "RHI_PATHS_REPO_LOCAL_ROOT", 271 "RadrootsRuntimeNamespace::worker", 272 "RhiRuntimeStartupReport", 273 "RhiRuntimeContractOutput", 274 "default_config_path_for_process", 275 "default_identity_path_for_process", 276 "default_subscriber_state_path_for_process", 277 ] { 278 assert!(!lib.contains(forbidden)); 279 assert!(!context.contains(forbidden)); 280 assert!(!main.contains(forbidden)); 281 } 282 for removed in [ 283 "src/paths.rs", 284 "src/host_paths/mod.rs", 285 "src/cli.rs", 286 "src/config.rs", 287 ] { 288 assert!( 289 !Path::new(env!("CARGO_MANIFEST_DIR")).join(removed).exists(), 290 "legacy path authority remains at {removed}" 291 ); 292 } 293 }