services_hardening_operator_contract.rs (16426B)
1 #![forbid(unsafe_code)] 2 3 use serde_json::Value; 4 use sha2::{Digest, Sha256}; 5 use std::collections::BTreeSet; 6 7 const OPERATOR_CONTRACT: &str = 8 include_str!("../contracts/services_hardening/operator_contract.v1.json"); 9 const CONSUMER_ROOT: &str = include_str!("../.radroots-consumer-root"); 10 11 fn contract() -> Value { 12 serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract must be valid JSON") 13 } 14 15 fn sha256_hex(bytes: &[u8]) -> String { 16 Sha256::digest(bytes) 17 .iter() 18 .map(|byte| format!("{byte:02x}")) 19 .collect() 20 } 21 22 fn decision_sections_digest(value: &Value) -> String { 23 let sections = serde_json::json!({ 24 "identity_contract": value["admin"]["identity_contract"], 25 "model_wire_contract": value["admin"]["model_wire_contract"], 26 "models": value["admin"]["models"], 27 "mutation_contract": value["admin"]["mutation_contract"], 28 "pagination": value["admin"]["pagination"], 29 "path_parameters": value["admin"]["path_parameters"], 30 "report_contract": value["admin"]["report_contract"], 31 "types": value["admin"]["types"] 32 }); 33 sha256_hex(&serde_json::to_vec(§ions).expect("serialize decision sections")) 34 } 35 36 #[test] 37 fn source_lock_identity_and_shared_host_reference_are_exact() { 38 assert_eq!(CONSUMER_ROOT, "rhi\n"); 39 let value = contract(); 40 assert_eq!(value["schema"], "radroots.rhi.operator-contract.v1"); 41 assert_eq!(value["contract_version"], 1); 42 assert_eq!(value["decision_state"], "reserved_preimplementation"); 43 assert_eq!(value["service"], "rhi"); 44 assert_eq!( 45 value["shared_host_contract"], 46 serde_json::json!({ 47 "repository": "https://github.com/radrootslabs/lib", 48 "path": "contracts/architecture/decisions/services_hardening_host.v1.json", 49 "schema": "radroots.services-hardening.host-decisions.v1", 50 "contract_version": 1 51 }) 52 ); 53 } 54 55 #[test] 56 fn admin_inventory_is_closed_unique_and_model_complete() { 57 let value = contract(); 58 assert_eq!( 59 value["admin"]["transport"], 60 "http_1_1_over_unix_domain_socket" 61 ); 62 assert_eq!(value["admin"]["base_path"], "/v1"); 63 assert_eq!(value["admin"]["route_inventory_closed"], true); 64 let routes = value["admin"]["routes"].as_array().expect("routes"); 65 let exact_routes = routes 66 .iter() 67 .map(|route| { 68 format!( 69 "{}|{}|{}|{}|{}|{}", 70 route["method"].as_str().unwrap(), 71 route["path"].as_str().unwrap(), 72 route["operation_id"].as_str().unwrap(), 73 route["request_model"].as_str().unwrap(), 74 route["response_model"].as_str().unwrap(), 75 route["mutation"].as_bool().unwrap() 76 ) 77 }) 78 .collect::<Vec<_>>(); 79 assert_eq!( 80 exact_routes, 81 [ 82 "GET|/v1/status|radroots.rhi.status.get.v1|empty|service_status_v1|false", 83 "GET|/v1/config/effective|radroots.rhi.config.effective.get.v1|empty|effective_config_v1|false", 84 "GET|/v1/identity/status|radroots.rhi.identity.status.get.v1|identity_status_query_v1|identity_status_v1|false", 85 "GET|/v1/identity/public|radroots.rhi.identity.public.get.v1|identity_public_query_v1|identity_public_v1|false", 86 "GET|/v1/state/status|radroots.rhi.state.status.get.v1|empty|state_status_v1|false", 87 "POST|/v1/state/backup|radroots.rhi.state.backup.create.v1|state_backup_request_v1|state_backup_receipt_v1|true", 88 "GET|/v1/metrics/snapshot|radroots.rhi.metrics.snapshot.get.v1|empty|metrics_snapshot_v1|false", 89 "GET|/v1/reconciliation/status|radroots.rhi.reconciliation.status.get.v1|empty|reconciliation_status_v1|false", 90 "GET|/v1/reconciliation/jobs|radroots.rhi.reconciliation.jobs.list.v1|reconciliation_jobs_query_v1|reconciliation_jobs_page_v1|false", 91 "POST|/v1/reconciliation/refresh|radroots.rhi.reconciliation.refresh.v1|reconciliation_refresh_request_v1|reconciliation_refresh_receipt_v1|true", 92 "GET|/v1/sources|radroots.rhi.sources.list.v1|sources_query_v1|sources_page_v1|false", 93 "GET|/v1/trades/{trade_id}/projection|radroots.rhi.trade.projection.get.v1|empty|trade_projection_v1|false", 94 "GET|/v1/trades/{trade_id}/reports/current|radroots.rhi.trade.report.current.get.v1|empty|report_detail_v1|false", 95 "GET|/v1/trades/{trade_id}/reports|radroots.rhi.trade.reports.list.v1|reports_query_v1|reports_page_v1|false", 96 "GET|/v1/publication/backlog|radroots.rhi.publication.backlog.list.v1|publication_backlog_query_v1|publication_backlog_page_v1|false", 97 "GET|/v1/publication/targets|radroots.rhi.publication.targets.list.v1|publication_targets_query_v1|publication_targets_page_v1|false", 98 "POST|/v1/publication/retry|radroots.rhi.publication.retry.v1|publication_retry_request_v1|publication_retry_receipt_v1|true", 99 "GET|/v1/presence/desired|radroots.rhi.presence.desired.get.v1|empty|presence_desired_v1|false", 100 "POST|/v1/presence/render|radroots.rhi.presence.render.v1|presence_render_request_v1|presence_render_receipt_v1|true", 101 "POST|/v1/presence/refresh|radroots.rhi.presence.refresh.v1|presence_refresh_request_v1|presence_refresh_receipt_v1|true" 102 ] 103 ); 104 assert_eq!(routes.len(), 20); 105 let route_keys = routes 106 .iter() 107 .map(|route| format!("{} {}", route["method"], route["path"])) 108 .collect::<BTreeSet<_>>(); 109 let operation_ids = routes 110 .iter() 111 .map(|route| route["operation_id"].as_str().expect("operation ID")) 112 .collect::<BTreeSet<_>>(); 113 assert_eq!(route_keys.len(), routes.len()); 114 assert_eq!(operation_ids.len(), routes.len()); 115 assert!( 116 operation_ids 117 .iter() 118 .all(|id| id.starts_with("radroots.rhi.") && id.ends_with(".v1")) 119 ); 120 121 let models = value["admin"]["models"].as_object().expect("models"); 122 let types = value["admin"]["types"].as_object().expect("types"); 123 assert_eq!(models.len(), 33); 124 for route in routes { 125 for key in ["request_model", "response_model"] { 126 let model = route[key].as_str().expect("model reference"); 127 assert!(models.contains_key(model), "missing model {model}"); 128 } 129 assert_eq!(route["mutation"], route["method"] == "POST"); 130 } 131 for (model_name, model) in models { 132 assert_eq!( 133 model 134 .as_object() 135 .unwrap() 136 .keys() 137 .map(String::as_str) 138 .collect::<Vec<_>>(), 139 ["fields"], 140 "model {model_name} must be a closed field inventory" 141 ); 142 for (field_name, field) in model["fields"].as_object().unwrap() { 143 assert_eq!( 144 field 145 .as_object() 146 .unwrap() 147 .keys() 148 .map(String::as_str) 149 .collect::<Vec<_>>(), 150 ["presence", "type"], 151 "field {model_name}.{field_name} must bind only type and presence" 152 ); 153 assert!(matches!( 154 field["presence"].as_str(), 155 Some("required" | "optional") 156 )); 157 let type_name = field["type"].as_str().unwrap(); 158 assert!(types.contains_key(type_name), "unknown type {type_name}"); 159 } 160 } 161 for (type_name, descriptor) in types { 162 let referenced = match descriptor["kind"].as_str().unwrap() { 163 "array" => vec![descriptor["items"].as_str().unwrap()], 164 "map" => vec![ 165 descriptor["key"].as_str().unwrap(), 166 descriptor["value"].as_str().unwrap(), 167 ], 168 "closed_object" => descriptor["fields"] 169 .as_object() 170 .unwrap() 171 .values() 172 .map(|field| field.as_str().unwrap()) 173 .collect(), 174 "tagged_union" => descriptor["variants"] 175 .as_array() 176 .unwrap() 177 .iter() 178 .map(|variant| variant.as_str().unwrap()) 179 .collect(), 180 "alias" => vec![descriptor["target"].as_str().unwrap()], 181 "optional" => vec![descriptor["value"].as_str().unwrap()], 182 "boolean" | "canonical_json_object" | "enum" | "integer" | "literal" | "string" => { 183 Vec::new() 184 } 185 kind => panic!("unknown descriptor kind {kind} for {type_name}"), 186 }; 187 for reference in referenced { 188 assert!( 189 types.contains_key(reference), 190 "type {type_name} references missing type {reference}" 191 ); 192 } 193 } 194 assert_eq!( 195 value["admin"]["identity_contract"], 196 serde_json::json!({ 197 "roles": [{ "id": "service", "required": true, "disabled_allowed": false, "providers": ["encrypted_file"] }], 198 "rotation_mode": "offline_create_new_configuration_apply_restart", 199 "live_rekey_route": false, 200 "live_replace_route": false 201 }) 202 ); 203 for removed in [ 204 "identity_rekey_request_v1", 205 "identity_replace_request_v1", 206 "identity_mutation_receipt_v1", 207 ] { 208 assert!(!models.contains_key(removed)); 209 } 210 for removed in [ 211 "credential_reference", 212 "encrypted_file_provider", 213 "identity_provider_replacement", 214 "encrypted_file_replacement", 215 ] { 216 assert!(!types.contains_key(removed)); 217 } 218 assert_eq!( 219 value["admin"]["types"]["service_phase"], 220 serde_json::json!({ "kind": "enum", "values": ["starting", "ready", "degraded", "unready", "stopping", "failed"] }) 221 ); 222 assert_eq!( 223 value["admin"]["types"]["coverage"], 224 serde_json::json!({ "kind": "enum", "values": ["Missing", "Partial", "ScopeSatisfied", "Unsupported"] }) 225 ); 226 assert_eq!( 227 value["admin"]["types"]["outcome"], 228 serde_json::json!({ "kind": "enum", "values": ["Valid", "Invalid", "Indeterminate"] }) 229 ); 230 assert_eq!( 231 value["admin"]["types"]["provider_state"]["fields"], 232 serde_json::json!({ 233 "health": "provider_health", 234 "identity": "identity_health", 235 "reason_codes": "reason_codes" 236 }) 237 ); 238 assert_eq!( 239 value["admin"]["types"]["transport_state"]["fields"], 240 serde_json::json!({ 241 "health": "transport_health", 242 "required_sources_ready": "bool", 243 "subscriber_active": "bool", 244 "configured_source_count": "u64", 245 "reachable_source_count": "u64", 246 "reason_codes": "reason_codes" 247 }) 248 ); 249 assert_eq!( 250 value["admin"]["types"]["rhi_status"]["fields"], 251 serde_json::json!({ 252 "identity": "identity_health", 253 "reconciliation": "reconciliation_status", 254 "publication": "publication_status", 255 "presence": "presence_status" 256 }) 257 ); 258 assert_eq!( 259 value["admin"]["models"]["service_status_v1"]["fields"] 260 .as_object() 261 .unwrap() 262 .keys() 263 .map(String::as_str) 264 .collect::<Vec<_>>(), 265 [ 266 "build_info", 267 "configuration", 268 "contract_version", 269 "instance", 270 "persistence", 271 "phase", 272 "provider", 273 "ready", 274 "reason_codes", 275 "rhi", 276 "service", 277 "transport", 278 "uptime_millis" 279 ] 280 ); 281 assert_eq!( 282 value["admin"]["path_parameters"], 283 serde_json::json!({ 284 "trade_id": { "type": "trade_id", "source": "percent_decoded_single_path_segment", "slash_allowed": false } 285 }) 286 ); 287 assert_eq!( 288 value["admin"]["report_contract"], 289 serde_json::json!({ 290 "content_contract": "radroots.rhi.evidence_attestation.v1", 291 "report_id_equals_statement_digest": true, 292 "supersession_reference_presence": "supersedes_report_id_and_supersedes_event_id_both_or_neither", 293 "current_selection": ["trade_generation_descending", "observed_at_unix_s_descending", "statement_digest_descending"], 294 "relay_arrival_order_authoritative": false 295 }) 296 ); 297 assert_eq!( 298 value["admin"]["pagination"], 299 serde_json::json!({ 300 "cursor_type": "page_cursor", 301 "limit_min": 1, 302 "limit_max": 200, 303 "terminal_page": "next_cursor_field_absent", 304 "cursor_reuse": "same_route_same_filters_only", 305 "filter_or_route_mismatch": "invalid_cursor", 306 "jobs_order": ["scheduled_at_utc_ascending", "job_id_ascending"], 307 "jobs_snapshot": "maximum_job_sequence_fixed_by_first_page_cursor", 308 "sources_order": ["source_id_ascending"], 309 "sources_snapshot": "configuration_generation_fixed_by_first_page_cursor", 310 "reports_order": ["observed_at_utc_descending", "report_id_descending"], 311 "reports_snapshot": "maximum_report_sequence_fixed_by_first_page_cursor", 312 "publication_backlog_order": ["next_attempt_at_utc_ascending_nulls_first", "workflow_id_ascending"], 313 "publication_backlog_snapshot": "maximum_publication_sequence_fixed_by_first_page_cursor", 314 "publication_targets_order": ["workflow_id_ascending", "target_id_ascending"], 315 "publication_targets_snapshot": "maximum_publication_target_sequence_fixed_by_first_page_cursor" 316 }) 317 ); 318 let mutation_operations = routes 319 .iter() 320 .filter(|route| route["mutation"] == true) 321 .map(|route| route["operation_id"].as_str().unwrap()) 322 .collect::<BTreeSet<_>>(); 323 let committed_effects = value["admin"]["mutation_contract"]["committed_effects"] 324 .as_object() 325 .unwrap() 326 .keys() 327 .map(String::as_str) 328 .collect::<BTreeSet<_>>(); 329 assert_eq!(mutation_operations, committed_effects); 330 assert_eq!( 331 decision_sections_digest(&value), 332 "49376e3bdf0e44c35f877ecd382f26bc9c38fec8fea7a674aa7d4da52ee00c62" 333 ); 334 } 335 336 #[test] 337 fn doctor_exit_and_tcp_contracts_are_exact() { 338 let value = contract(); 339 assert_eq!( 340 value["doctor"]["shared_schema"], 341 "radroots.service.doctor.v1" 342 ); 343 assert_eq!(value["doctor"]["contract_version"], 1); 344 assert_eq!(value["doctor"]["execution"], "ordered"); 345 assert_eq!(value["doctor"]["pass_requires_all_scope"], true); 346 assert_eq!( 347 value["doctor"]["checks"] 348 .as_array() 349 .expect("doctor checks") 350 .len(), 351 15 352 ); 353 assert_eq!( 354 value["exit_codes"], 355 serde_json::json!([ 356 { "code": 0, "name": "success", "meaning": "successful command or completed graceful first-signal shutdown" }, 357 { "code": 1, "name": "unexpected_internal", "meaning": "unexpected invariant, critical task, or internal failure" }, 358 { "code": 2, "name": "input_or_configuration", "meaning": "CLI, config, validation, or unsupported contract input" }, 359 { "code": 3, "name": "service_or_dependency_unavailable", "meaning": "daemon, required provider, relay, source, or local dependency unavailable" }, 360 { "code": 4, "name": "state_or_identity_unavailable", "meaning": "state, schema, lock, credential, or identity unavailable" }, 361 { "code": 5, "name": "operation_rejected_or_conflict", "meaning": "authorization rejection, idempotency conflict, stale generation, or domain conflict" }, 362 { "code": 6, "name": "doctor_required_check_failed", "meaning": "one or more required doctor checks failed or timed out" } 363 ]) 364 ); 365 assert_eq!( 366 value["tcp_operations"], 367 serde_json::json!({ 368 "routes": [ 369 { "method": "GET", "path": "/livez", "source": "cached_supervisor_state" }, 370 { "method": "GET", "path": "/readyz", "source": "cached_readiness_state" }, 371 { "method": "GET", "path": "/metrics", "source": "cached_bounded_metrics_snapshot" } 372 ], 373 "active_probe_per_request": false, 374 "additional_routes": false 375 }) 376 ); 377 }