admin_v1.rs (75498B)
1 //! Exact Myc v1 Unix-admin route and model boundary. 2 3 use core::{fmt, future::Future, pin::Pin, time::Duration}; 4 use std::{ 5 collections::BTreeSet, 6 error::Error, 7 path::Path, 8 sync::{Arc, OnceLock}, 9 }; 10 11 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 12 use radroots_service_host::{ 13 AdminCorrelationId, AdminError, AdminErrorCode, AdminErrorMessage, AdminHttpMethod, 14 AdminMutationRequest, AdminOperationId, AdminRequest, AdminRouteFailure, 15 AdminRouteFailureStatus, AdminRouteOutcome, AdminRouter, AdminServer, AdminServerError, 16 AdminTransportLimitValues, AdminTransportLimits, CancellationToken, UnixAdminSocketBinding, 17 UnixAdminSocketWriterAuthority, 18 }; 19 use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor}; 20 use serde_json::{Map, Value}; 21 22 // Original model admission is never permitted to exceed the complete response 23 // body cap enforced again by the shared host after envelope encoding. 24 const MYC_ADMIN_RESPONSE_BODY_MAX_UTF8_BYTES: usize = 1_048_576; 25 26 const OPERATOR_CONTRACT: &str = 27 include_str!("../contracts/services_hardening/operator_contract.v1.json"); 28 29 /// Closed Myc v1 admin method vocabulary. 30 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 31 pub enum MycAdminMethod { 32 Get, 33 Post, 34 } 35 36 /// Closed Myc v1 Unix-admin route inventory. 37 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 38 pub enum MycAdminRoute { 39 Status, 40 EffectiveConfig, 41 IdentityStatus, 42 IdentityPublic, 43 StateStatus, 44 StateBackup, 45 MetricsSnapshot, 46 ConnectionsList, 47 ConnectionApprove, 48 ConnectionReject, 49 ConnectionRevoke, 50 ChallengeRequire, 51 ChallengeAuthorize, 52 AuditEvents, 53 AuditSummary, 54 DiscoveryDesired, 55 DiscoveryRender, 56 DiscoveryRefresh, 57 DiscoveryPublish, 58 } 59 60 impl MycAdminRoute { 61 pub const ALL: [Self; 19] = [ 62 Self::Status, 63 Self::EffectiveConfig, 64 Self::IdentityStatus, 65 Self::IdentityPublic, 66 Self::StateStatus, 67 Self::StateBackup, 68 Self::MetricsSnapshot, 69 Self::ConnectionsList, 70 Self::ConnectionApprove, 71 Self::ConnectionReject, 72 Self::ConnectionRevoke, 73 Self::ChallengeRequire, 74 Self::ChallengeAuthorize, 75 Self::AuditEvents, 76 Self::AuditSummary, 77 Self::DiscoveryDesired, 78 Self::DiscoveryRender, 79 Self::DiscoveryRefresh, 80 Self::DiscoveryPublish, 81 ]; 82 83 #[must_use] 84 pub const fn method(self) -> MycAdminMethod { 85 match self { 86 Self::Status 87 | Self::EffectiveConfig 88 | Self::IdentityStatus 89 | Self::IdentityPublic 90 | Self::StateStatus 91 | Self::MetricsSnapshot 92 | Self::ConnectionsList 93 | Self::AuditEvents 94 | Self::AuditSummary 95 | Self::DiscoveryDesired => MycAdminMethod::Get, 96 Self::StateBackup 97 | Self::ConnectionApprove 98 | Self::ConnectionReject 99 | Self::ConnectionRevoke 100 | Self::ChallengeRequire 101 | Self::ChallengeAuthorize 102 | Self::DiscoveryRender 103 | Self::DiscoveryRefresh 104 | Self::DiscoveryPublish => MycAdminMethod::Post, 105 } 106 } 107 108 #[must_use] 109 pub const fn path(self) -> &'static str { 110 match self { 111 Self::Status => "/v1/status", 112 Self::EffectiveConfig => "/v1/config/effective", 113 Self::IdentityStatus => "/v1/identity/status", 114 Self::IdentityPublic => "/v1/identity/public", 115 Self::StateStatus => "/v1/state/status", 116 Self::StateBackup => "/v1/state/backup", 117 Self::MetricsSnapshot => "/v1/metrics/snapshot", 118 Self::ConnectionsList => "/v1/connections", 119 Self::ConnectionApprove => "/v1/connections/{connection_id}/approve", 120 Self::ConnectionReject => "/v1/connections/{connection_id}/reject", 121 Self::ConnectionRevoke => "/v1/connections/{connection_id}/revoke", 122 Self::ChallengeRequire => "/v1/authorization/challenges/require", 123 Self::ChallengeAuthorize => "/v1/authorization/challenges/{challenge_id}/authorize", 124 Self::AuditEvents => "/v1/audit/events", 125 Self::AuditSummary => "/v1/audit/summary", 126 Self::DiscoveryDesired => "/v1/discovery/desired", 127 Self::DiscoveryRender => "/v1/discovery/render", 128 Self::DiscoveryRefresh => "/v1/discovery/refresh", 129 Self::DiscoveryPublish => "/v1/discovery/publish", 130 } 131 } 132 133 #[must_use] 134 pub const fn operation_id(self) -> &'static str { 135 match self { 136 Self::Status => "radroots.myc.status.get.v1", 137 Self::EffectiveConfig => "radroots.myc.config.effective.get.v1", 138 Self::IdentityStatus => "radroots.myc.identity.status.get.v1", 139 Self::IdentityPublic => "radroots.myc.identity.public.get.v1", 140 Self::StateStatus => "radroots.myc.state.status.get.v1", 141 Self::StateBackup => "radroots.myc.state.backup.create.v1", 142 Self::MetricsSnapshot => "radroots.myc.metrics.snapshot.get.v1", 143 Self::ConnectionsList => "radroots.myc.connections.list.v1", 144 Self::ConnectionApprove => "radroots.myc.connection.approve.v1", 145 Self::ConnectionReject => "radroots.myc.connection.reject.v1", 146 Self::ConnectionRevoke => "radroots.myc.connection.revoke.v1", 147 Self::ChallengeRequire => "radroots.myc.authorization.challenge.require.v1", 148 Self::ChallengeAuthorize => "radroots.myc.authorization.challenge.authorize.v1", 149 Self::AuditEvents => "radroots.myc.audit.events.list.v1", 150 Self::AuditSummary => "radroots.myc.audit.summary.get.v1", 151 Self::DiscoveryDesired => "radroots.myc.discovery.desired.get.v1", 152 Self::DiscoveryRender => "radroots.myc.discovery.render.v1", 153 Self::DiscoveryRefresh => "radroots.myc.discovery.refresh.v1", 154 Self::DiscoveryPublish => "radroots.myc.discovery.publish.v1", 155 } 156 } 157 158 #[must_use] 159 pub const fn request_model(self) -> &'static str { 160 match self { 161 Self::Status 162 | Self::EffectiveConfig 163 | Self::StateStatus 164 | Self::MetricsSnapshot 165 | Self::DiscoveryDesired => "empty", 166 Self::IdentityStatus => "identity_status_query_v1", 167 Self::IdentityPublic => "identity_public_query_v1", 168 Self::StateBackup => "state_backup_request_v1", 169 Self::ConnectionsList => "connections_query_v1", 170 Self::ConnectionApprove => "connection_approve_request_v1", 171 Self::ConnectionReject => "connection_reject_request_v1", 172 Self::ConnectionRevoke => "connection_revoke_request_v1", 173 Self::ChallengeRequire => "challenge_require_request_v1", 174 Self::ChallengeAuthorize => "challenge_authorize_request_v1", 175 Self::AuditEvents => "audit_events_query_v1", 176 Self::AuditSummary => "audit_summary_query_v1", 177 Self::DiscoveryRender => "discovery_render_request_v1", 178 Self::DiscoveryRefresh => "discovery_refresh_request_v1", 179 Self::DiscoveryPublish => "discovery_publish_request_v1", 180 } 181 } 182 183 #[must_use] 184 pub const fn response_model(self) -> &'static str { 185 match self { 186 Self::Status => "service_status_v1", 187 Self::EffectiveConfig => "effective_config_v1", 188 Self::IdentityStatus => "identity_status_v1", 189 Self::IdentityPublic => "identity_public_v1", 190 Self::StateStatus => "state_status_v1", 191 Self::StateBackup => "state_backup_receipt_v1", 192 Self::MetricsSnapshot => "metrics_snapshot_v1", 193 Self::ConnectionsList => "connections_page_v1", 194 Self::ConnectionApprove | Self::ConnectionReject | Self::ConnectionRevoke => { 195 "connection_mutation_receipt_v1" 196 } 197 Self::ChallengeRequire => "challenge_v1", 198 Self::ChallengeAuthorize => "challenge_authorization_receipt_v1", 199 Self::AuditEvents => "audit_events_page_v1", 200 Self::AuditSummary => "audit_summary_v1", 201 Self::DiscoveryDesired => "discovery_desired_v1", 202 Self::DiscoveryRender => "discovery_render_receipt_v1", 203 Self::DiscoveryRefresh => "discovery_refresh_receipt_v1", 204 Self::DiscoveryPublish => "discovery_publish_receipt_v1", 205 } 206 } 207 208 #[must_use] 209 pub const fn is_mutation(self) -> bool { 210 matches!(self.method(), MycAdminMethod::Post) 211 } 212 213 const fn host_method(self) -> AdminHttpMethod { 214 match self.method() { 215 MycAdminMethod::Get => AdminHttpMethod::Get, 216 MycAdminMethod::Post => AdminHttpMethod::Post, 217 } 218 } 219 220 const fn parameter_name(self) -> Option<&'static str> { 221 match self { 222 Self::ConnectionApprove | Self::ConnectionReject | Self::ConnectionRevoke => { 223 Some("connection_id") 224 } 225 Self::ChallengeAuthorize => Some("challenge_id"), 226 _ => None, 227 } 228 } 229 } 230 231 /// One route-bound, already validated Myc admin request. 232 pub struct MycAdminRequestDocument { 233 route: MycAdminRoute, 234 operation_id: Option<AdminOperationId>, 235 correlation_id: AdminCorrelationId, 236 parameter: Option<(&'static str, Box<str>)>, 237 model_bytes: Box<[u8]>, 238 } 239 240 impl MycAdminRequestDocument { 241 #[must_use] 242 pub const fn route(&self) -> MycAdminRoute { 243 self.route 244 } 245 246 /// Returns the caller's durable idempotency identity for a mutation. 247 #[must_use] 248 pub fn operation_id(&self) -> Option<&str> { 249 self.operation_id.as_ref().map(AdminOperationId::as_str) 250 } 251 252 #[must_use] 253 pub fn correlation_id(&self) -> &str { 254 self.correlation_id.as_str() 255 } 256 257 /// Returns a validated percent-decoded path parameter when this route has one. 258 #[must_use] 259 pub fn parameter(&self, name: &str) -> Option<&str> { 260 self.parameter 261 .as_ref() 262 .filter(|(parameter_name, _)| *parameter_name == name) 263 .map(|(_, value)| value.as_ref()) 264 } 265 266 pub(crate) fn parameter_binding(&self) -> Option<(&'static str, &str)> { 267 self.parameter 268 .as_ref() 269 .map(|(name, value)| (*name, value.as_ref())) 270 } 271 272 /// Returns compact canonical JSON for the route's exact request model. 273 #[must_use] 274 pub fn model_bytes(&self) -> &[u8] { 275 &self.model_bytes 276 } 277 278 #[cfg(test)] 279 pub(crate) fn mutation_for_test( 280 route: MycAdminRoute, 281 operation_id: &str, 282 parameter: Option<(&'static str, &str)>, 283 model_bytes: &[u8], 284 ) -> Self { 285 assert!(route.is_mutation()); 286 Self { 287 route, 288 operation_id: Some(AdminOperationId::new(operation_id).expect("test operation ID")), 289 correlation_id: AdminCorrelationId::new("test-correlation") 290 .expect("test correlation ID"), 291 parameter: parameter.map(|(name, value)| (name, value.into())), 292 model_bytes: model_bytes.into(), 293 } 294 } 295 } 296 297 impl fmt::Debug for MycAdminRequestDocument { 298 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 299 formatter 300 .debug_struct("MycAdminRequestDocument") 301 .field("route", &self.route) 302 .field("mutation", &self.operation_id.is_some()) 303 .field("has_parameter", &self.parameter.is_some()) 304 .field("model", &"[redacted]") 305 .finish() 306 } 307 } 308 309 /// One exact validated response model for a fixed route. 310 pub struct MycAdminResponseDocument { 311 route: MycAdminRoute, 312 canonical_bytes: Box<[u8]>, 313 value: Value, 314 } 315 316 impl MycAdminResponseDocument { 317 /// Admits only compact canonical JSON matching the route's response model. 318 pub fn from_canonical_bytes( 319 route: MycAdminRoute, 320 bytes: &[u8], 321 ) -> Result<Self, MycAdminDocumentError> { 322 if bytes.is_empty() { 323 return Err(MycAdminDocumentError::new( 324 MycAdminDocumentErrorKind::Malformed, 325 )); 326 } 327 if bytes.len() > MYC_ADMIN_RESPONSE_BODY_MAX_UTF8_BYTES { 328 return Err(MycAdminDocumentError::new( 329 MycAdminDocumentErrorKind::TooLarge, 330 )); 331 } 332 let value = strict_json(bytes)?; 333 validate_model(route.response_model(), &value)?; 334 let canonical = serde_json::to_vec(&value) 335 .map_err(|_| MycAdminDocumentError::new(MycAdminDocumentErrorKind::Malformed))?; 336 if canonical.as_slice() != bytes { 337 return Err(MycAdminDocumentError::new( 338 MycAdminDocumentErrorKind::NonCanonical, 339 )); 340 } 341 Ok(Self { 342 route, 343 canonical_bytes: canonical.into_boxed_slice(), 344 value, 345 }) 346 } 347 348 #[must_use] 349 pub const fn route(&self) -> MycAdminRoute { 350 self.route 351 } 352 353 #[must_use] 354 pub fn canonical_bytes(&self) -> &[u8] { 355 &self.canonical_bytes 356 } 357 } 358 359 impl fmt::Debug for MycAdminResponseDocument { 360 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 361 formatter 362 .debug_struct("MycAdminResponseDocument") 363 .field("route", &self.route) 364 .field("model", &"[redacted]") 365 .finish() 366 } 367 } 368 369 /// Stable classification for a rejected Myc admin document. 370 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 371 pub enum MycAdminDocumentErrorKind { 372 TooLarge, 373 Malformed, 374 DuplicateField, 375 NullForbidden, 376 NonCanonical, 377 InvalidModel, 378 } 379 380 /// Source-free and content-free Myc admin document error. 381 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 382 pub struct MycAdminDocumentError { 383 kind: MycAdminDocumentErrorKind, 384 } 385 386 impl MycAdminDocumentError { 387 const fn new(kind: MycAdminDocumentErrorKind) -> Self { 388 Self { kind } 389 } 390 391 #[must_use] 392 pub const fn kind(self) -> MycAdminDocumentErrorKind { 393 self.kind 394 } 395 } 396 397 impl fmt::Display for MycAdminDocumentError { 398 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 399 formatter.write_str("Myc admin document is invalid") 400 } 401 } 402 403 impl Error for MycAdminDocumentError {} 404 405 /// Stable route-handler failure mapped to a bounded safe admin response. 406 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 407 pub enum MycAdminHandlerErrorKind { 408 InvalidCursor, 409 OperationIdConflict, 410 NotFound, 411 Conflict, 412 Unavailable, 413 Internal, 414 } 415 416 /// Source-free route-handler error. 417 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 418 pub struct MycAdminHandlerError { 419 kind: MycAdminHandlerErrorKind, 420 } 421 422 impl MycAdminHandlerError { 423 #[must_use] 424 pub const fn new(kind: MycAdminHandlerErrorKind) -> Self { 425 Self { kind } 426 } 427 428 #[must_use] 429 pub const fn kind(self) -> MycAdminHandlerErrorKind { 430 self.kind 431 } 432 } 433 434 impl fmt::Display for MycAdminHandlerError { 435 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 436 formatter.write_str("Myc admin operation failed") 437 } 438 } 439 440 impl Error for MycAdminHandlerError {} 441 442 /// Boxed route future used by the sealed Myc admin adapter. 443 pub type MycAdminFuture<'a> = Pin< 444 Box<dyn Future<Output = Result<MycAdminResponseDocument, MycAdminHandlerError>> + Send + 'a>, 445 >; 446 447 /// Domain port behind the exact Myc admin transport. 448 /// 449 /// Implementations own authoritative local commit, durable operation-ID 450 /// replay/conflict handling, and any provider or outbox orchestration. Returning 451 /// success means that the operation's contract-defined local effect is already 452 /// committed; relay submission or delivery is not implied. Pagination cursors 453 /// must be authenticated and bound to the same route, filters, and snapshot; 454 /// mismatched or invalid cursors return [`MycAdminHandlerErrorKind::InvalidCursor`]. 455 /// Reusing an operation ID with identical canonical request bytes returns the 456 /// original committed response; reuse with different bytes returns 457 /// [`MycAdminHandlerErrorKind::OperationIdConflict`]. 458 pub trait MycAdminHandler: Send + Sync + 'static { 459 fn handle<'a>(&'a self, request: MycAdminRequestDocument) -> MycAdminFuture<'a>; 460 } 461 462 /// Source-free router-construction failure. 463 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 464 pub struct MycAdminRouterError; 465 466 impl fmt::Display for MycAdminRouterError { 467 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 468 formatter.write_str("Myc admin router could not be constructed") 469 } 470 } 471 472 impl Error for MycAdminRouterError {} 473 474 /// Opaque, fully registered Myc v1 router capability. 475 /// 476 /// The underlying shared-host router remains an implementation detail. The 477 /// later runtime-composition checkpoint consumes this capability without 478 /// exposing raw listener or transport authority. 479 pub struct MycAdminRouter { 480 inner: AdminRouter, 481 } 482 483 impl fmt::Debug for MycAdminRouter { 484 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 485 let Self { inner } = self; 486 let _ = inner; 487 formatter.write_str("MycAdminRouter") 488 } 489 } 490 491 impl MycAdminRouter { 492 fn into_inner(self) -> AdminRouter { 493 self.inner 494 } 495 } 496 497 /// Cloneable cooperative cancellation for the Myc Unix-admin server. 498 #[derive(Clone, Default)] 499 pub struct MycAdminCancellationToken { 500 inner: CancellationToken, 501 } 502 503 impl MycAdminCancellationToken { 504 #[must_use] 505 pub fn new() -> Self { 506 Self::default() 507 } 508 509 /// Requests cancellation. Repeated requests have no additional effect. 510 pub fn cancel(&self) { 511 self.inner.cancel(); 512 } 513 514 #[must_use] 515 pub fn is_cancelled(&self) -> bool { 516 self.inner.is_cancelled() 517 } 518 } 519 520 impl fmt::Debug for MycAdminCancellationToken { 521 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 522 formatter 523 .debug_struct("MycAdminCancellationToken") 524 .field("cancelled", &self.is_cancelled()) 525 .finish() 526 } 527 } 528 529 /// Stable source-free Myc Unix-admin server failure classification. 530 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 531 pub enum MycAdminServerErrorKind { 532 InvalidConfiguration, 533 Router, 534 ServerConfiguration, 535 WriterAuthority, 536 Bind, 537 Listener, 538 Accept, 539 ConnectionTaskPanicked, 540 } 541 542 impl MycAdminServerErrorKind { 543 #[must_use] 544 pub const fn code(self) -> &'static str { 545 match self { 546 Self::InvalidConfiguration => "admin_configuration_invalid", 547 Self::Router => "admin_router_invalid", 548 Self::ServerConfiguration => "admin_server_configuration_invalid", 549 Self::WriterAuthority => "admin_writer_authority_unavailable", 550 Self::Bind => "admin_bind_failed", 551 Self::Listener => "admin_listener_failed", 552 Self::Accept => "admin_accept_failed", 553 Self::ConnectionTaskPanicked => "admin_connection_task_panicked", 554 } 555 } 556 } 557 558 /// One redacted source-free Myc Unix-admin server failure. 559 #[derive(Clone, Copy, PartialEq, Eq)] 560 pub struct MycAdminServerError { 561 kind: MycAdminServerErrorKind, 562 } 563 564 impl MycAdminServerError { 565 const fn new(kind: MycAdminServerErrorKind) -> Self { 566 Self { kind } 567 } 568 569 #[must_use] 570 pub const fn kind(self) -> MycAdminServerErrorKind { 571 self.kind 572 } 573 574 #[must_use] 575 pub const fn code(self) -> &'static str { 576 self.kind.code() 577 } 578 } 579 580 impl fmt::Debug for MycAdminServerError { 581 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 582 formatter 583 .debug_struct("MycAdminServerError") 584 .field("kind", &self.kind) 585 .finish() 586 } 587 } 588 589 impl fmt::Display for MycAdminServerError { 590 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 591 formatter.write_str("Myc Unix-admin server failed") 592 } 593 } 594 595 impl Error for MycAdminServerError {} 596 597 /// Unbound production Myc Unix-admin server. 598 /// 599 /// Construction projects only the already-admitted Myc configuration, seals 600 /// the exact route inventory around the supplied domain handler, and uses the 601 /// shared host's system entropy. The raw shared router and server never cross 602 /// this boundary. 603 pub struct MycAdminServer { 604 inner: AdminServer, 605 } 606 607 impl MycAdminServer { 608 pub fn new<H>( 609 configuration: &crate::MycConfigDocumentV1, 610 handler: Arc<H>, 611 ) -> Result<Self, MycAdminServerError> 612 where 613 H: MycAdminHandler, 614 { 615 let limits = admin_transport_limits(configuration)?; 616 let router = build_myc_admin_router(handler) 617 .map_err(|_| MycAdminServerError::new(MycAdminServerErrorKind::Router))?; 618 let inner = AdminServer::with_system_entropy(router.into_inner(), limits) 619 .map_err(|_| MycAdminServerError::new(MycAdminServerErrorKind::ServerConfiguration))?; 620 Ok(Self { inner }) 621 } 622 623 /// Acquires the canonical runtime-directory authority and binds `admin.sock`. 624 /// 625 /// Binding does not spawn a task or begin request admission. Unit 15 owns 626 /// the final supervised server task and its shutdown phase. 627 pub async fn bind( 628 self, 629 runtime: &crate::MycRuntimeContext, 630 ) -> Result<MycBoundAdminServer, MycAdminServerError> { 631 let authority = UnixAdminSocketWriterAuthority::acquire(runtime.context().paths().run()) 632 .map_err(|_| MycAdminServerError::new(MycAdminServerErrorKind::WriterAuthority))?; 633 let binding = UnixAdminSocketBinding::bind(authority, runtime.artifacts().admin_socket()) 634 .await 635 .map_err(|_| MycAdminServerError::new(MycAdminServerErrorKind::Bind))?; 636 Ok(MycBoundAdminServer { 637 inner: self.inner, 638 binding, 639 }) 640 } 641 } 642 643 impl fmt::Debug for MycAdminServer { 644 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 645 formatter.write_str("MycAdminServer([sealed])") 646 } 647 } 648 649 /// Bound production Myc Unix-admin server. 650 pub struct MycBoundAdminServer { 651 inner: AdminServer, 652 binding: UnixAdminSocketBinding, 653 } 654 655 impl MycBoundAdminServer { 656 /// Serves until supervisor cancellation and then drains bounded connection work. 657 pub async fn serve( 658 self, 659 cancellation: MycAdminCancellationToken, 660 ) -> Result<(), MycAdminServerError> { 661 self.inner 662 .serve(self.binding, cancellation.inner) 663 .await 664 .map_err(map_admin_server_error) 665 } 666 } 667 668 impl fmt::Debug for MycBoundAdminServer { 669 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 670 formatter.write_str("MycBoundAdminServer([sealed])") 671 } 672 } 673 674 /// Registers the complete closed Myc v1 route inventory on the hardened Lib router. 675 pub fn build_myc_admin_router<H>(handler: Arc<H>) -> Result<MycAdminRouter, MycAdminRouterError> 676 where 677 H: MycAdminHandler, 678 { 679 if !operator_route_inventory_is_exact() { 680 return Err(MycAdminRouterError); 681 } 682 let mut router = AdminRouter::new(); 683 for route in MycAdminRoute::ALL { 684 let handler = Arc::clone(&handler); 685 router 686 .route(route.host_method(), route.path(), move |request| { 687 let handler = Arc::clone(&handler); 688 async move { dispatch_route(route, handler, request).await } 689 }) 690 .map_err(|_| MycAdminRouterError)?; 691 } 692 Ok(MycAdminRouter { inner: router }) 693 } 694 695 pub(crate) fn admin_transport_limits( 696 configuration: &crate::MycConfigDocumentV1, 697 ) -> Result<AdminTransportLimits, MycAdminServerError> { 698 let admin = configuration 699 .normalized() 700 .pointer("/resource_limits/admin") 701 .ok_or_else(invalid_admin_configuration)?; 702 let values = AdminTransportLimitValues { 703 header_count: admin_u32(admin, "/header_count")?, 704 header_bytes: admin_u32(admin, "/header_bytes")?, 705 request_body_utf8_bytes: admin_u32(admin, "/request_body_utf8_bytes")?, 706 response_body_utf8_bytes: admin_u32(admin, "/response_body_utf8_bytes")?, 707 concurrent_connections: admin_u32(admin, "/concurrent_connections")?, 708 request_deadline: Duration::from_millis(admin_u64(admin, "/request_deadline_ms")?), 709 idle_timeout: Duration::from_millis(admin_u64(admin, "/idle_timeout_ms")?), 710 query_items: admin_u32(admin, "/query_items")?, 711 }; 712 AdminTransportLimits::new(values).map_err(|_| invalid_admin_configuration()) 713 } 714 715 pub(crate) fn admit_admin_response_value( 716 route: MycAdminRoute, 717 value: &Value, 718 ) -> Result<Box<[u8]>, MycAdminDocumentError> { 719 let bytes = serde_json::to_vec(value) 720 .map_err(|_| MycAdminDocumentError::new(MycAdminDocumentErrorKind::Malformed))?; 721 MycAdminResponseDocument::from_canonical_bytes(route, &bytes) 722 .map(|document| document.canonical_bytes) 723 } 724 725 fn admin_u64(value: &Value, pointer: &str) -> Result<u64, MycAdminServerError> { 726 value 727 .pointer(pointer) 728 .and_then(Value::as_u64) 729 .ok_or_else(invalid_admin_configuration) 730 } 731 732 fn admin_u32(value: &Value, pointer: &str) -> Result<u32, MycAdminServerError> { 733 u32::try_from(admin_u64(value, pointer)?).map_err(|_| invalid_admin_configuration()) 734 } 735 736 const fn invalid_admin_configuration() -> MycAdminServerError { 737 MycAdminServerError::new(MycAdminServerErrorKind::InvalidConfiguration) 738 } 739 740 const fn map_admin_server_error(error: AdminServerError) -> MycAdminServerError { 741 let kind = match error { 742 AdminServerError::ListenerClone { .. } | AdminServerError::ListenerRegistration { .. } => { 743 MycAdminServerErrorKind::Listener 744 } 745 AdminServerError::Accept { .. } => MycAdminServerErrorKind::Accept, 746 AdminServerError::ConnectionTaskPanicked => MycAdminServerErrorKind::ConnectionTaskPanicked, 747 }; 748 MycAdminServerError::new(kind) 749 } 750 751 async fn dispatch_route<H>( 752 route: MycAdminRoute, 753 handler: Arc<H>, 754 request: AdminRequest, 755 ) -> AdminRouteOutcome 756 where 757 H: MycAdminHandler, 758 { 759 let document = match request_document(route, &request) { 760 Ok(document) => document, 761 Err(_) => return failure(MycAdminHandlerErrorKind::Conflict, true), 762 }; 763 match handler.handle(document).await { 764 Ok(response) if response.route == route => match request.success(&response.value) { 765 Ok(outcome) => outcome, 766 Err(_) => failure(MycAdminHandlerErrorKind::Internal, false), 767 }, 768 Ok(_) => failure(MycAdminHandlerErrorKind::Internal, false), 769 Err(error) => failure(error.kind, false), 770 } 771 } 772 773 fn failure(kind: MycAdminHandlerErrorKind, invalid_request: bool) -> AdminRouteOutcome { 774 let (status, code, message) = if invalid_request { 775 ( 776 AdminRouteFailureStatus::BadRequest, 777 "invalid_request", 778 "admin request does not match the route model", 779 ) 780 } else { 781 match kind { 782 MycAdminHandlerErrorKind::InvalidCursor => ( 783 AdminRouteFailureStatus::BadRequest, 784 "invalid_cursor", 785 "admin pagination cursor is invalid", 786 ), 787 MycAdminHandlerErrorKind::OperationIdConflict => ( 788 AdminRouteFailureStatus::Conflict, 789 "operation_id_conflict", 790 "admin operation identity conflicts with retained state", 791 ), 792 MycAdminHandlerErrorKind::NotFound => ( 793 AdminRouteFailureStatus::NotFound, 794 "not_found", 795 "admin resource was not found", 796 ), 797 MycAdminHandlerErrorKind::Conflict => ( 798 AdminRouteFailureStatus::Conflict, 799 "operation_conflict", 800 "admin operation conflicts with current state", 801 ), 802 MycAdminHandlerErrorKind::Unavailable => ( 803 AdminRouteFailureStatus::Unavailable, 804 "service_unavailable", 805 "admin operation is temporarily unavailable", 806 ), 807 MycAdminHandlerErrorKind::Internal => ( 808 AdminRouteFailureStatus::Internal, 809 "internal_error", 810 "admin operation failed internally", 811 ), 812 } 813 }; 814 let code = AdminErrorCode::new(code).expect("fixed admin error code"); 815 let message = AdminErrorMessage::new(message).expect("fixed admin error message"); 816 AdminRouteOutcome::failure(AdminRouteFailure::new( 817 status, 818 AdminError::new(code, message), 819 )) 820 } 821 822 fn request_document( 823 route: MycAdminRoute, 824 request: &AdminRequest, 825 ) -> Result<MycAdminRequestDocument, MycAdminDocumentError> { 826 let (operation_id, value) = match route.method() { 827 MycAdminMethod::Get => (None, query_model(route, request.query())?), 828 MycAdminMethod::Post => { 829 let envelope = request 830 .decode_json::<AdminMutationRequest<Value>>() 831 .map_err(|_| MycAdminDocumentError::new(MycAdminDocumentErrorKind::Malformed))?; 832 ( 833 Some(envelope.operation_id().clone()), 834 envelope.into_request(), 835 ) 836 } 837 }; 838 validate_model(route.request_model(), &value)?; 839 let model_bytes = serde_json::to_vec(&value) 840 .map_err(|_| MycAdminDocumentError::new(MycAdminDocumentErrorKind::Malformed))?; 841 let parameter = route 842 .parameter_name() 843 .map(|name| { 844 let value = request 845 .parameter(name) 846 .ok_or_else(|| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel))? 847 .to_owned() 848 .into_boxed_str(); 849 validate_type("bounded_id", &Value::String(value.to_string()), 0)?; 850 Ok((name, value)) 851 }) 852 .transpose()?; 853 Ok(MycAdminRequestDocument { 854 route, 855 operation_id, 856 correlation_id: request.correlation_id().clone(), 857 parameter, 858 model_bytes: model_bytes.into_boxed_slice(), 859 }) 860 } 861 862 fn query_model(route: MycAdminRoute, query: Option<&str>) -> Result<Value, MycAdminDocumentError> { 863 let Some(query) = query else { 864 return Ok(Value::Object(Map::new())); 865 }; 866 if query.is_empty() { 867 return Err(MycAdminDocumentError::new( 868 MycAdminDocumentErrorKind::InvalidModel, 869 )); 870 } 871 let fields = model_fields(route.request_model())?; 872 let mut output = Map::new(); 873 for item in query.split('&') { 874 let (raw_key, raw_value) = item 875 .split_once('=') 876 .ok_or_else(|| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel))?; 877 if raw_key.is_empty() 878 || !valid_percent_encoding(raw_key) 879 || !valid_percent_encoding(raw_value) 880 { 881 return Err(MycAdminDocumentError::new( 882 MycAdminDocumentErrorKind::InvalidModel, 883 )); 884 } 885 let key = percent_decode(raw_key)?; 886 let value = percent_decode(raw_value)?; 887 if output.contains_key(&key) { 888 return Err(MycAdminDocumentError::new( 889 MycAdminDocumentErrorKind::DuplicateField, 890 )); 891 } 892 let descriptor = fields 893 .get(&key) 894 .ok_or_else(|| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel))?; 895 let type_name = descriptor 896 .get("type") 897 .and_then(Value::as_str) 898 .ok_or_else(|| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel))?; 899 output.insert(key, query_scalar(type_name, value)?); 900 } 901 Ok(Value::Object(output)) 902 } 903 904 fn query_scalar(type_name: &str, value: String) -> Result<Value, MycAdminDocumentError> { 905 let descriptor = type_descriptor(type_name)?; 906 match descriptor.get("kind").and_then(Value::as_str) { 907 Some("integer") => { 908 let canonical = value == "0" 909 || (value.as_bytes().first().is_some_and(u8::is_ascii_digit) 910 && !value.starts_with('0') 911 && value.bytes().all(|byte| byte.is_ascii_digit())); 912 if !canonical { 913 return Err(MycAdminDocumentError::new( 914 MycAdminDocumentErrorKind::InvalidModel, 915 )); 916 } 917 value 918 .parse::<u64>() 919 .map(Value::from) 920 .map_err(|_| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel)) 921 } 922 Some("boolean") => match value.as_str() { 923 "true" => Ok(Value::Bool(true)), 924 "false" => Ok(Value::Bool(false)), 925 _ => Err(MycAdminDocumentError::new( 926 MycAdminDocumentErrorKind::InvalidModel, 927 )), 928 }, 929 _ => Ok(Value::String(value)), 930 } 931 } 932 933 fn percent_decode(value: &str) -> Result<String, MycAdminDocumentError> { 934 let bytes = value.as_bytes(); 935 let mut decoded = Vec::with_capacity(bytes.len()); 936 let mut index = 0; 937 while index < bytes.len() { 938 match bytes[index] { 939 b'%' => { 940 let high = hex_nibble(bytes[index + 1]).ok_or_else(invalid_model_error)?; 941 let low = hex_nibble(bytes[index + 2]).ok_or_else(invalid_model_error)?; 942 decoded.push((high << 4) | low); 943 index += 3; 944 } 945 b'+' => { 946 decoded.push(b' '); 947 index += 1; 948 } 949 byte => { 950 decoded.push(byte); 951 index += 1; 952 } 953 } 954 } 955 String::from_utf8(decoded).map_err(|_| invalid_model_error()) 956 } 957 958 const fn hex_nibble(byte: u8) -> Option<u8> { 959 match byte { 960 b'0'..=b'9' => Some(byte - b'0'), 961 b'a'..=b'f' => Some(byte - b'a' + 10), 962 b'A'..=b'F' => Some(byte - b'A' + 10), 963 _ => None, 964 } 965 } 966 967 fn valid_percent_encoding(value: &str) -> bool { 968 let bytes = value.as_bytes(); 969 let mut index = 0; 970 while index < bytes.len() { 971 if bytes[index] == b'%' { 972 if index + 2 >= bytes.len() 973 || !bytes[index + 1].is_ascii_hexdigit() 974 || !bytes[index + 2].is_ascii_hexdigit() 975 { 976 return false; 977 } 978 index += 3; 979 } else { 980 index += 1; 981 } 982 } 983 true 984 } 985 986 fn operator_contract() -> &'static Value { 987 static CONTRACT: OnceLock<Value> = OnceLock::new(); 988 CONTRACT.get_or_init(|| { 989 serde_json::from_str(OPERATOR_CONTRACT).expect("checked-in Myc operator contract") 990 }) 991 } 992 993 fn operator_route_inventory_is_exact() -> bool { 994 let Some(admin) = operator_contract().get("admin") else { 995 return false; 996 }; 997 let Some(routes) = admin.get("routes").and_then(Value::as_array) else { 998 return false; 999 }; 1000 let Some(models) = admin.get("models").and_then(Value::as_object) else { 1001 return false; 1002 }; 1003 routes.len() == MycAdminRoute::ALL.len() 1004 && models.len() == 32 1005 && admin 1006 .pointer("/model_wire_contract/response_body_max_utf8_bytes") 1007 .and_then(Value::as_u64) 1008 == Some(MYC_ADMIN_RESPONSE_BODY_MAX_UTF8_BYTES as u64) 1009 && routes.iter().zip(MycAdminRoute::ALL).all(|(wire, route)| { 1010 wire.get("method").and_then(Value::as_str) 1011 == Some(match route.method() { 1012 MycAdminMethod::Get => "GET", 1013 MycAdminMethod::Post => "POST", 1014 }) 1015 && wire.get("path").and_then(Value::as_str) == Some(route.path()) 1016 && wire.get("operation_id").and_then(Value::as_str) == Some(route.operation_id()) 1017 && wire.get("request_model").and_then(Value::as_str) == Some(route.request_model()) 1018 && wire.get("response_model").and_then(Value::as_str) 1019 == Some(route.response_model()) 1020 && wire.get("mutation").and_then(Value::as_bool) == Some(route.is_mutation()) 1021 }) 1022 } 1023 1024 fn model_fields(model_name: &str) -> Result<&'static Map<String, Value>, MycAdminDocumentError> { 1025 operator_contract() 1026 .pointer(&format!("/admin/models/{model_name}/fields")) 1027 .and_then(Value::as_object) 1028 .ok_or_else(|| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel)) 1029 } 1030 1031 fn type_descriptor(type_name: &str) -> Result<&'static Value, MycAdminDocumentError> { 1032 operator_contract() 1033 .pointer(&format!("/admin/types/{type_name}")) 1034 .ok_or_else(|| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel)) 1035 } 1036 1037 fn validate_model(model_name: &str, value: &Value) -> Result<(), MycAdminDocumentError> { 1038 let object = value 1039 .as_object() 1040 .ok_or_else(|| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel))?; 1041 let fields = model_fields(model_name)?; 1042 for key in object.keys() { 1043 if !fields.contains_key(key) { 1044 return Err(MycAdminDocumentError::new( 1045 MycAdminDocumentErrorKind::InvalidModel, 1046 )); 1047 } 1048 } 1049 for (name, field) in fields { 1050 let required = field.get("presence").and_then(Value::as_str) == Some("required"); 1051 let type_name = field 1052 .get("type") 1053 .and_then(Value::as_str) 1054 .ok_or_else(|| MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel))?; 1055 match object.get(name) { 1056 Some(value) => validate_type(type_name, value, 0)?, 1057 None if required => { 1058 return Err(MycAdminDocumentError::new( 1059 MycAdminDocumentErrorKind::InvalidModel, 1060 )); 1061 } 1062 None => {} 1063 } 1064 } 1065 Ok(()) 1066 } 1067 1068 fn validate_type( 1069 type_name: &str, 1070 value: &Value, 1071 depth: usize, 1072 ) -> Result<(), MycAdminDocumentError> { 1073 if depth > 24 || value.is_null() { 1074 return Err(MycAdminDocumentError::new(if value.is_null() { 1075 MycAdminDocumentErrorKind::NullForbidden 1076 } else { 1077 MycAdminDocumentErrorKind::InvalidModel 1078 })); 1079 } 1080 let descriptor = type_descriptor(type_name)?; 1081 match descriptor.get("kind").and_then(Value::as_str) { 1082 Some("literal") => { 1083 if descriptor.get("value") != Some(value) { 1084 return invalid_model(); 1085 } 1086 } 1087 Some("boolean") => { 1088 if !value.is_boolean() { 1089 return invalid_model(); 1090 } 1091 } 1092 Some("integer") => validate_integer(descriptor, value)?, 1093 Some("string") => validate_string(descriptor, value)?, 1094 Some("enum") => { 1095 if !descriptor 1096 .get("values") 1097 .and_then(Value::as_array) 1098 .is_some_and(|values| values.contains(value)) 1099 { 1100 return invalid_model(); 1101 } 1102 } 1103 Some("string_union") => validate_string_union(descriptor, value)?, 1104 Some("array") => validate_array(descriptor, value, depth + 1)?, 1105 Some("canonical_delimited_set") => { 1106 validate_delimited_set(descriptor, value, depth + 1)?; 1107 } 1108 Some("map") => validate_map(descriptor, value, depth + 1)?, 1109 Some("closed_object") => validate_closed_object(descriptor, value, depth + 1)?, 1110 Some("tagged_union") => validate_tagged_union(descriptor, value, depth + 1)?, 1111 Some("alias") => validate_type( 1112 descriptor 1113 .get("target") 1114 .and_then(Value::as_str) 1115 .ok_or_else(invalid_model_error)?, 1116 value, 1117 depth + 1, 1118 )?, 1119 Some("optional") => validate_type( 1120 descriptor 1121 .get("value") 1122 .and_then(Value::as_str) 1123 .ok_or_else(invalid_model_error)?, 1124 value, 1125 depth + 1, 1126 )?, 1127 Some("canonical_json_object") => { 1128 if !value.is_object() 1129 || serde_json::to_vec(value).ok().is_none_or(|bytes| { 1130 bytes.len() 1131 > descriptor 1132 .get("maximum_utf8_bytes") 1133 .and_then(Value::as_u64) 1134 .and_then(|value| usize::try_from(value).ok()) 1135 .unwrap_or(0) 1136 }) 1137 { 1138 return invalid_model(); 1139 } 1140 } 1141 _ => return invalid_model(), 1142 } 1143 Ok(()) 1144 } 1145 1146 fn validate_integer(descriptor: &Value, value: &Value) -> Result<(), MycAdminDocumentError> { 1147 let number = value.as_u64().ok_or_else(invalid_model_error)?; 1148 let minimum = descriptor 1149 .get("minimum") 1150 .and_then(Value::as_u64) 1151 .unwrap_or(0); 1152 let maximum = descriptor 1153 .get("maximum") 1154 .and_then(Value::as_u64) 1155 .unwrap_or(u64::MAX); 1156 if !(minimum..=maximum).contains(&number) { 1157 return invalid_model(); 1158 } 1159 Ok(()) 1160 } 1161 1162 fn validate_string(descriptor: &Value, value: &Value) -> Result<(), MycAdminDocumentError> { 1163 let string = value.as_str().ok_or_else(invalid_model_error)?; 1164 let exact = descriptor 1165 .get("utf8_bytes") 1166 .and_then(Value::as_u64) 1167 .and_then(|value| usize::try_from(value).ok()); 1168 let minimum = descriptor 1169 .get("minimum_utf8_bytes") 1170 .and_then(Value::as_u64) 1171 .and_then(|value| usize::try_from(value).ok()) 1172 .unwrap_or(0); 1173 let maximum = descriptor 1174 .get("maximum_utf8_bytes") 1175 .and_then(Value::as_u64) 1176 .and_then(|value| usize::try_from(value).ok()) 1177 .unwrap_or(usize::MAX); 1178 if exact.is_some_and(|exact| string.len() != exact) 1179 || !(minimum..=maximum).contains(&string.len()) 1180 || string.chars().any(char::is_control) 1181 { 1182 return invalid_model(); 1183 } 1184 if let Some(pattern) = descriptor.get("pattern").and_then(Value::as_str) { 1185 let valid = match pattern { 1186 "^[A-Za-z0-9][A-Za-z0-9._:-]*$" => bounded_id(string), 1187 "^[a-z][a-z0-9_]*$" => safe_code(string), 1188 "^[0-9a-f]{64}$" => lower_hex(string, 64), 1189 "^[0-9a-f]{40}$" => lower_hex(string, 40), 1190 "^/" => Path::new(string).is_absolute(), 1191 _ => false, 1192 }; 1193 if !valid { 1194 return invalid_model(); 1195 } 1196 } 1197 if descriptor.get("encoding").and_then(Value::as_str) == Some("canonical_base64url_no_padding") 1198 { 1199 let decoded = URL_SAFE_NO_PAD 1200 .decode(string) 1201 .map_err(|_| invalid_model_error())?; 1202 if URL_SAFE_NO_PAD.encode(decoded) != string { 1203 return invalid_model(); 1204 } 1205 } 1206 if let Some(schemes) = descriptor.get("allowed_schemes").and_then(Value::as_array) { 1207 let parsed = url::Url::parse(string).map_err(|_| invalid_model_error())?; 1208 if !schemes 1209 .iter() 1210 .any(|scheme| scheme.as_str() == Some(parsed.scheme())) 1211 { 1212 return invalid_model(); 1213 } 1214 } 1215 Ok(()) 1216 } 1217 1218 fn validate_string_union(descriptor: &Value, value: &Value) -> Result<(), MycAdminDocumentError> { 1219 let string = value.as_str().ok_or_else(invalid_model_error)?; 1220 if descriptor 1221 .get("simple_values") 1222 .and_then(Value::as_array) 1223 .is_some_and(|values| values.iter().any(|value| value.as_str() == Some(string))) 1224 { 1225 return Ok(()); 1226 } 1227 let kind = string 1228 .strip_prefix("sign_event:kind:") 1229 .ok_or_else(invalid_model_error)?; 1230 if kind.is_empty() 1231 || (kind.len() > 1 && kind.starts_with('0')) 1232 || !kind.bytes().all(|byte| byte.is_ascii_digit()) 1233 || kind.parse::<u32>().is_err() 1234 || string.len() 1235 > descriptor 1236 .get("maximum_utf8_bytes") 1237 .and_then(Value::as_u64) 1238 .and_then(|value| usize::try_from(value).ok()) 1239 .unwrap_or(0) 1240 { 1241 return invalid_model(); 1242 } 1243 Ok(()) 1244 } 1245 1246 fn validate_array( 1247 descriptor: &Value, 1248 value: &Value, 1249 depth: usize, 1250 ) -> Result<(), MycAdminDocumentError> { 1251 let values = value.as_array().ok_or_else(invalid_model_error)?; 1252 let maximum = descriptor 1253 .get("maximum_items") 1254 .and_then(Value::as_u64) 1255 .and_then(|value| usize::try_from(value).ok()) 1256 .unwrap_or(0); 1257 if values.len() > maximum { 1258 return invalid_model(); 1259 } 1260 let item_type = descriptor 1261 .get("items") 1262 .and_then(Value::as_str) 1263 .ok_or_else(invalid_model_error)?; 1264 for item in values { 1265 validate_type(item_type, item, depth)?; 1266 } 1267 if descriptor.get("unique").and_then(Value::as_bool) == Some(true) { 1268 let mut unique = BTreeSet::new(); 1269 for item in values { 1270 let encoded = serde_json::to_vec(item).map_err(|_| invalid_model_error())?; 1271 if !unique.insert(encoded) { 1272 return invalid_model(); 1273 } 1274 } 1275 } 1276 if descriptor.get("canonical_sort").is_some() { 1277 let rendered = values 1278 .iter() 1279 .map(|value| value.as_str().ok_or_else(invalid_model_error)) 1280 .collect::<Result<Vec<_>, _>>()?; 1281 if !rendered.windows(2).all(|pair| pair[0] < pair[1]) { 1282 return invalid_model(); 1283 } 1284 } 1285 Ok(()) 1286 } 1287 1288 fn validate_delimited_set( 1289 descriptor: &Value, 1290 value: &Value, 1291 depth: usize, 1292 ) -> Result<(), MycAdminDocumentError> { 1293 let rendered = value.as_str().ok_or_else(invalid_model_error)?; 1294 let maximum_bytes = descriptor 1295 .get("maximum_utf8_bytes") 1296 .and_then(Value::as_u64) 1297 .and_then(|value| usize::try_from(value).ok()) 1298 .unwrap_or(0); 1299 if rendered.len() > maximum_bytes { 1300 return invalid_model(); 1301 } 1302 if rendered.is_empty() { 1303 return if descriptor.get("empty_allowed").and_then(Value::as_bool) == Some(true) { 1304 Ok(()) 1305 } else { 1306 invalid_model() 1307 }; 1308 } 1309 let delimiter = descriptor 1310 .get("delimiter") 1311 .and_then(Value::as_str) 1312 .filter(|delimiter| delimiter.len() == 1) 1313 .ok_or_else(invalid_model_error)?; 1314 let items = rendered.split(delimiter).collect::<Vec<_>>(); 1315 let maximum_items = descriptor 1316 .get("maximum_items") 1317 .and_then(Value::as_u64) 1318 .and_then(|value| usize::try_from(value).ok()) 1319 .unwrap_or(0); 1320 if items.is_empty() 1321 || items.len() > maximum_items 1322 || items.iter().any(|item| item.is_empty()) 1323 || !items.windows(2).all(|pair| pair[0] < pair[1]) 1324 { 1325 return invalid_model(); 1326 } 1327 let item_type = descriptor 1328 .get("items") 1329 .and_then(Value::as_str) 1330 .ok_or_else(invalid_model_error)?; 1331 for item in items { 1332 validate_type(item_type, &Value::String(item.to_owned()), depth)?; 1333 } 1334 Ok(()) 1335 } 1336 1337 fn validate_map( 1338 descriptor: &Value, 1339 value: &Value, 1340 depth: usize, 1341 ) -> Result<(), MycAdminDocumentError> { 1342 let values = value.as_object().ok_or_else(invalid_model_error)?; 1343 let maximum_entries = descriptor 1344 .get("maximum_entries") 1345 .and_then(Value::as_u64) 1346 .and_then(|value| usize::try_from(value).ok()) 1347 .unwrap_or(0); 1348 if values.len() > maximum_entries { 1349 return invalid_model(); 1350 } 1351 let key_type = descriptor 1352 .get("key") 1353 .and_then(Value::as_str) 1354 .ok_or_else(invalid_model_error)?; 1355 let value_type = descriptor 1356 .get("value") 1357 .and_then(Value::as_str) 1358 .ok_or_else(invalid_model_error)?; 1359 for (key, value) in values { 1360 validate_type(key_type, &Value::String(key.clone()), depth)?; 1361 validate_type(value_type, value, depth)?; 1362 } 1363 Ok(()) 1364 } 1365 1366 fn validate_closed_object( 1367 descriptor: &Value, 1368 value: &Value, 1369 depth: usize, 1370 ) -> Result<(), MycAdminDocumentError> { 1371 let values = value.as_object().ok_or_else(invalid_model_error)?; 1372 let fields = descriptor 1373 .get("fields") 1374 .and_then(Value::as_object) 1375 .ok_or_else(invalid_model_error)?; 1376 if values.keys().any(|key| !fields.contains_key(key)) { 1377 return invalid_model(); 1378 } 1379 for (field, type_name) in fields { 1380 let type_name = type_name.as_str().ok_or_else(invalid_model_error)?; 1381 match values.get(field) { 1382 Some(value) => validate_type(type_name, value, depth)?, 1383 None if type_descriptor(type_name)? 1384 .get("kind") 1385 .and_then(Value::as_str) 1386 == Some("optional") => {} 1387 None => return invalid_model(), 1388 } 1389 } 1390 Ok(()) 1391 } 1392 1393 fn validate_tagged_union( 1394 descriptor: &Value, 1395 value: &Value, 1396 depth: usize, 1397 ) -> Result<(), MycAdminDocumentError> { 1398 let object = value.as_object().ok_or_else(invalid_model_error)?; 1399 let discriminator = descriptor 1400 .get("discriminator") 1401 .and_then(Value::as_str) 1402 .ok_or_else(invalid_model_error)?; 1403 let selected = object.get(discriminator).ok_or_else(invalid_model_error)?; 1404 let variants = descriptor 1405 .get("variants") 1406 .and_then(Value::as_array) 1407 .ok_or_else(invalid_model_error)?; 1408 let mut matched = None; 1409 for variant in variants { 1410 let variant = variant.as_str().ok_or_else(invalid_model_error)?; 1411 let fields = type_descriptor(variant)? 1412 .get("fields") 1413 .and_then(Value::as_object) 1414 .ok_or_else(invalid_model_error)?; 1415 let discriminator_type = fields 1416 .get(discriminator) 1417 .and_then(Value::as_str) 1418 .ok_or_else(invalid_model_error)?; 1419 if type_descriptor(discriminator_type)?.get("value") == Some(selected) 1420 && matched.replace(variant).is_some() 1421 { 1422 return invalid_model(); 1423 } 1424 } 1425 validate_type(matched.ok_or_else(invalid_model_error)?, value, depth) 1426 } 1427 1428 fn bounded_id(value: &str) -> bool { 1429 (1..=128).contains(&value.len()) 1430 && value 1431 .as_bytes() 1432 .first() 1433 .is_some_and(u8::is_ascii_alphanumeric) 1434 && value 1435 .bytes() 1436 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) 1437 } 1438 1439 fn safe_code(value: &str) -> bool { 1440 (1..=64).contains(&value.len()) 1441 && value.as_bytes().first().is_some_and(u8::is_ascii_lowercase) 1442 && value 1443 .bytes() 1444 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') 1445 } 1446 1447 fn lower_hex(value: &str, exact: usize) -> bool { 1448 value.len() == exact 1449 && value 1450 .bytes() 1451 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) 1452 } 1453 1454 fn invalid_model<T>() -> Result<T, MycAdminDocumentError> { 1455 Err(invalid_model_error()) 1456 } 1457 1458 const fn invalid_model_error() -> MycAdminDocumentError { 1459 MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel) 1460 } 1461 1462 const DUPLICATE_MARKER: &str = "myc_admin_duplicate_field"; 1463 const NULL_MARKER: &str = "myc_admin_null_forbidden"; 1464 1465 fn strict_json(bytes: &[u8]) -> Result<Value, MycAdminDocumentError> { 1466 let mut deserializer = serde_json::Deserializer::from_slice(bytes); 1467 let value = StrictValueSeed 1468 .deserialize(&mut deserializer) 1469 .map_err(|error| { 1470 let message = error.to_string(); 1471 if message.contains(DUPLICATE_MARKER) { 1472 MycAdminDocumentError::new(MycAdminDocumentErrorKind::DuplicateField) 1473 } else if message.contains(NULL_MARKER) { 1474 MycAdminDocumentError::new(MycAdminDocumentErrorKind::NullForbidden) 1475 } else { 1476 MycAdminDocumentError::new(MycAdminDocumentErrorKind::Malformed) 1477 } 1478 })?; 1479 deserializer 1480 .end() 1481 .map_err(|_| MycAdminDocumentError::new(MycAdminDocumentErrorKind::Malformed))?; 1482 Ok(value) 1483 } 1484 1485 struct StrictValueSeed; 1486 1487 impl<'de> DeserializeSeed<'de> for StrictValueSeed { 1488 type Value = Value; 1489 1490 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error> 1491 where 1492 D: serde::Deserializer<'de>, 1493 { 1494 deserializer.deserialize_any(StrictValueVisitor) 1495 } 1496 } 1497 1498 struct StrictValueVisitor; 1499 1500 impl<'de> Visitor<'de> for StrictValueVisitor { 1501 type Value = Value; 1502 1503 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 1504 formatter.write_str("a non-null JSON value") 1505 } 1506 1507 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> { 1508 Ok(Value::Bool(value)) 1509 } 1510 1511 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> { 1512 Ok(Value::from(value)) 1513 } 1514 1515 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> { 1516 Ok(Value::from(value)) 1517 } 1518 1519 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> 1520 where 1521 E: de::Error, 1522 { 1523 serde_json::Number::from_f64(value) 1524 .map(Value::Number) 1525 .ok_or_else(|| E::custom("invalid JSON number")) 1526 } 1527 1528 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> { 1529 Ok(Value::String(value.to_owned())) 1530 } 1531 1532 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> { 1533 Ok(Value::String(value)) 1534 } 1535 1536 fn visit_none<E>(self) -> Result<Self::Value, E> 1537 where 1538 E: de::Error, 1539 { 1540 Err(E::custom(NULL_MARKER)) 1541 } 1542 1543 fn visit_unit<E>(self) -> Result<Self::Value, E> 1544 where 1545 E: de::Error, 1546 { 1547 Err(E::custom(NULL_MARKER)) 1548 } 1549 1550 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error> 1551 where 1552 A: SeqAccess<'de>, 1553 { 1554 let mut values = Vec::new(); 1555 while let Some(value) = sequence.next_element_seed(StrictValueSeed)? { 1556 values.push(value); 1557 } 1558 Ok(Value::Array(values)) 1559 } 1560 1561 fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error> 1562 where 1563 A: MapAccess<'de>, 1564 { 1565 let mut values = Map::new(); 1566 while let Some(key) = object.next_key::<String>()? { 1567 if values.contains_key(&key) { 1568 return Err(de::Error::custom(DUPLICATE_MARKER)); 1569 } 1570 let value = object.next_value_seed(StrictValueSeed)?; 1571 values.insert(key, value); 1572 } 1573 Ok(Value::Object(values)) 1574 } 1575 } 1576 1577 #[cfg(test)] 1578 mod tests { 1579 use super::*; 1580 1581 fn sample_model(model_name: &str) -> Value { 1582 let fields = model_fields(model_name).expect("governed model"); 1583 let mut object = Map::new(); 1584 for (name, field) in fields { 1585 if field.get("presence").and_then(Value::as_str) == Some("required") { 1586 let type_name = field 1587 .get("type") 1588 .and_then(Value::as_str) 1589 .expect("field type"); 1590 object.insert(name.clone(), sample_type(type_name).expect("required type")); 1591 } 1592 } 1593 Value::Object(object) 1594 } 1595 1596 fn sample_type(type_name: &str) -> Option<Value> { 1597 let descriptor = type_descriptor(type_name).expect("governed type"); 1598 match descriptor 1599 .get("kind") 1600 .and_then(Value::as_str) 1601 .expect("type kind") 1602 { 1603 "literal" => Some(descriptor.get("value").expect("literal value").clone()), 1604 "boolean" => Some(Value::Bool(false)), 1605 "integer" => Some(Value::from( 1606 descriptor 1607 .get("minimum") 1608 .and_then(Value::as_u64) 1609 .unwrap_or(0), 1610 )), 1611 "string" => { 1612 let value = if descriptor.get("encoding").is_some() { 1613 "AA".to_owned() 1614 } else if descriptor.get("allowed_schemes").is_some() { 1615 "https://example.test/".to_owned() 1616 } else if descriptor.get("pattern").and_then(Value::as_str) == Some("^/") { 1617 "/tmp/myc".to_owned() 1618 } else if let Some(length) = descriptor.get("utf8_bytes").and_then(Value::as_u64) { 1619 "0".repeat(usize::try_from(length).expect("bounded sample length")) 1620 } else { 1621 "x".to_owned() 1622 }; 1623 Some(Value::String(value)) 1624 } 1625 "enum" => descriptor 1626 .get("values") 1627 .and_then(Value::as_array) 1628 .and_then(|values| values.first()) 1629 .cloned(), 1630 "string_union" => descriptor 1631 .get("simple_values") 1632 .and_then(Value::as_array) 1633 .and_then(|values| values.first()) 1634 .cloned(), 1635 "array" => Some(Value::Array(Vec::new())), 1636 "canonical_delimited_set" => Some(Value::String(String::new())), 1637 "map" | "canonical_json_object" => Some(Value::Object(Map::new())), 1638 "closed_object" => { 1639 let mut object = Map::new(); 1640 for (field, field_type) in descriptor 1641 .get("fields") 1642 .and_then(Value::as_object) 1643 .expect("closed fields") 1644 { 1645 if let Some(value) = sample_type(field_type.as_str().expect("closed type")) { 1646 object.insert(field.clone(), value); 1647 } 1648 } 1649 Some(Value::Object(object)) 1650 } 1651 "tagged_union" => descriptor 1652 .get("variants") 1653 .and_then(Value::as_array) 1654 .and_then(|variants| variants.first()) 1655 .and_then(Value::as_str) 1656 .and_then(sample_type), 1657 "alias" => descriptor 1658 .get("target") 1659 .and_then(Value::as_str) 1660 .and_then(sample_type), 1661 "optional" => None, 1662 kind => panic!("unsupported governed kind {kind}"), 1663 } 1664 } 1665 1666 fn response_document(route: MycAdminRoute) -> MycAdminResponseDocument { 1667 let value = sample_model(route.response_model()); 1668 validate_model(route.response_model(), &value).expect("generated response model"); 1669 let bytes = serde_json::to_vec(&value).expect("canonical response bytes"); 1670 MycAdminResponseDocument::from_canonical_bytes(route, &bytes) 1671 .expect("admitted response document") 1672 } 1673 1674 #[test] 1675 fn complete_route_and_model_inventory_matches_the_machine_contract() { 1676 assert!(operator_route_inventory_is_exact()); 1677 assert_eq!(MycAdminRoute::ALL.len(), 19); 1678 let referenced = MycAdminRoute::ALL 1679 .into_iter() 1680 .flat_map(|route| [route.request_model(), route.response_model()]) 1681 .collect::<BTreeSet<_>>(); 1682 let governed = operator_contract()["admin"]["models"] 1683 .as_object() 1684 .expect("model inventory") 1685 .keys() 1686 .map(String::as_str) 1687 .collect::<BTreeSet<_>>(); 1688 assert_eq!(referenced, governed); 1689 assert_eq!(governed.len(), 32); 1690 for model in governed { 1691 let value = sample_model(model); 1692 validate_model(model, &value).expect("minimum exact model"); 1693 } 1694 } 1695 1696 #[test] 1697 fn strict_response_admission_rejects_duplicates_null_and_noncanonical_bytes() { 1698 let route = MycAdminRoute::IdentityPublic; 1699 let valid = response_document(route); 1700 assert_eq!(valid.route(), route); 1701 assert!(!valid.canonical_bytes().is_empty()); 1702 1703 let duplicate = br#"{"generation":0,"generation":1,"public_key":"0000000000000000000000000000000000000000000000000000000000000000","role":"transport"}"#; 1704 assert_eq!( 1705 MycAdminResponseDocument::from_canonical_bytes(route, duplicate) 1706 .expect_err("duplicate key") 1707 .kind(), 1708 MycAdminDocumentErrorKind::DuplicateField 1709 ); 1710 let nested_null = br#"{"generation":0,"public_key":null,"role":"transport"}"#; 1711 assert_eq!( 1712 MycAdminResponseDocument::from_canonical_bytes(route, nested_null) 1713 .expect_err("nested null") 1714 .kind(), 1715 MycAdminDocumentErrorKind::NullForbidden 1716 ); 1717 let noncanonical = format!(" {}", String::from_utf8_lossy(valid.canonical_bytes())); 1718 assert_eq!( 1719 MycAdminResponseDocument::from_canonical_bytes(route, noncanonical.as_bytes()) 1720 .expect_err("whitespace") 1721 .kind(), 1722 MycAdminDocumentErrorKind::NonCanonical 1723 ); 1724 let invalid = br#"{"generation":0,"public_key":"0000000000000000000000000000000000000000000000000000000000000000","role":"administrator"}"#; 1725 assert_eq!( 1726 MycAdminResponseDocument::from_canonical_bytes(route, invalid) 1727 .expect_err("invalid model") 1728 .kind(), 1729 MycAdminDocumentErrorKind::InvalidModel 1730 ); 1731 assert_eq!( 1732 MycAdminResponseDocument::from_canonical_bytes(route, b"") 1733 .expect_err("empty response") 1734 .kind(), 1735 MycAdminDocumentErrorKind::Malformed 1736 ); 1737 let oversized = vec![b' '; MYC_ADMIN_RESPONSE_BODY_MAX_UTF8_BYTES + 1]; 1738 assert_eq!( 1739 MycAdminResponseDocument::from_canonical_bytes(route, &oversized) 1740 .expect_err("oversized response") 1741 .kind(), 1742 MycAdminDocumentErrorKind::TooLarge 1743 ); 1744 assert_eq!( 1745 format!("{valid:?}"), 1746 "MycAdminResponseDocument { route: IdentityPublic, model: \"[redacted]\" }" 1747 ); 1748 } 1749 1750 #[test] 1751 fn query_and_nested_type_admission_is_exact_and_bounded() { 1752 let connections = query_model( 1753 MycAdminRoute::ConnectionsList, 1754 Some("cursor=AA&limit=200&state=approved"), 1755 ) 1756 .expect("canonical query"); 1757 validate_model("connections_query_v1", &connections).expect("query model"); 1758 for invalid in [ 1759 "limit=01", 1760 "limit=201", 1761 "limit=1&limit=2", 1762 "unknown=x", 1763 "cursor=%", 1764 "state=administrator", 1765 ] { 1766 let result = query_model(MycAdminRoute::ConnectionsList, Some(invalid)) 1767 .and_then(|value| validate_model("connections_query_v1", &value)); 1768 assert!(result.is_err(), "query `{invalid}` unexpectedly passed"); 1769 } 1770 for valid in [ 1771 "", 1772 "nip04_decrypt", 1773 "sign_event:kind:0", 1774 "nip04_decrypt,sign_event:kind:1", 1775 ] { 1776 validate_type("permission_set", &Value::String(valid.to_owned()), 0) 1777 .expect("permission set"); 1778 } 1779 for invalid in [ 1780 "sign_event:kind:00", 1781 "sign_event:kind:4294967296", 1782 "sign_event:kind:1,nip04_decrypt", 1783 "nip04_decrypt,nip04_decrypt", 1784 ] { 1785 assert!( 1786 validate_type("permission_set", &Value::String(invalid.to_owned()), 0).is_err() 1787 ); 1788 } 1789 assert!( 1790 validate_type( 1791 "safe_url", 1792 &Value::String("http://example.test/".to_owned()), 1793 0 1794 ) 1795 .is_err() 1796 ); 1797 assert!(validate_type("bounded_id", &Value::String("../escape".to_owned()), 0).is_err()); 1798 } 1799 1800 #[test] 1801 fn public_diagnostics_are_source_free_and_content_free() { 1802 let document = MycAdminDocumentError::new(MycAdminDocumentErrorKind::InvalidModel); 1803 let handler = MycAdminHandlerError::new(MycAdminHandlerErrorKind::Internal); 1804 let server = MycAdminServerError::new(MycAdminServerErrorKind::Bind); 1805 for rendered in [ 1806 format!("{document}"), 1807 format!("{document:?}"), 1808 format!("{handler}"), 1809 format!("{handler:?}"), 1810 format!("{server}"), 1811 format!("{server:?}"), 1812 ] { 1813 assert!(!rendered.contains("/tmp/protected")); 1814 assert!(!rendered.contains("credential")); 1815 } 1816 assert!(Error::source(&document).is_none()); 1817 assert!(Error::source(&handler).is_none()); 1818 assert!(Error::source(&server).is_none()); 1819 assert_eq!(server.code(), "admin_bind_failed"); 1820 } 1821 1822 #[cfg(any(target_os = "linux", target_os = "macos"))] 1823 mod native { 1824 use core::sync::atomic::{AtomicUsize, Ordering}; 1825 use std::fs; 1826 use std::sync::Mutex; 1827 1828 use radroots_service_host::{AdminClient, AdminClientTarget, AdminTransportLimits}; 1829 1830 use super::*; 1831 use tokio::io::{AsyncReadExt, AsyncWriteExt}; 1832 1833 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml"); 1834 1835 type FixtureCall = (MycAdminRoute, Option<String>, Box<[u8]>); 1836 1837 struct FixtureHandler { 1838 calls: Mutex<Vec<FixtureCall>>, 1839 invalid_cursor: AtomicUsize, 1840 decoded_parameter: AtomicUsize, 1841 } 1842 1843 impl FixtureHandler { 1844 fn new() -> Self { 1845 Self { 1846 calls: Mutex::new(Vec::new()), 1847 invalid_cursor: AtomicUsize::new(0), 1848 decoded_parameter: AtomicUsize::new(0), 1849 } 1850 } 1851 } 1852 1853 impl MycAdminHandler for FixtureHandler { 1854 fn handle<'a>(&'a self, request: MycAdminRequestDocument) -> MycAdminFuture<'a> { 1855 Box::pin(async move { 1856 if request.route() == MycAdminRoute::ConnectionsList 1857 && request 1858 .model_bytes() 1859 .windows(4) 1860 .any(|window| window == b"\"eA\"") 1861 { 1862 self.invalid_cursor.fetch_add(1, Ordering::SeqCst); 1863 return Err(MycAdminHandlerError::new( 1864 MycAdminHandlerErrorKind::InvalidCursor, 1865 )); 1866 } 1867 if request.operation_id() == Some("conflict") { 1868 return Err(MycAdminHandlerError::new( 1869 MycAdminHandlerErrorKind::OperationIdConflict, 1870 )); 1871 } 1872 if request.parameter("connection_id") == Some("connection-encoded") { 1873 self.decoded_parameter.fetch_add(1, Ordering::SeqCst); 1874 return Ok(response_document(request.route())); 1875 } 1876 self.calls.lock().expect("calls").push(( 1877 request.route(), 1878 request.operation_id().map(str::to_owned), 1879 request.model_bytes().into(), 1880 )); 1881 Ok(response_document(request.route())) 1882 }) 1883 } 1884 } 1885 1886 fn runtime_context() -> ( 1887 tempfile::TempDir, 1888 crate::MycRuntimeContext, 1889 crate::MycConfigDocumentV1, 1890 ) { 1891 let root = tempfile::Builder::new() 1892 .prefix("myc-admin-") 1893 .tempdir_in("/tmp") 1894 .expect("short runtime root"); 1895 let root_path = root.path().to_str().expect("UTF-8 test root"); 1896 let invocation = crate::parse_myc_cli_v1_from([ 1897 "myc", 1898 "--profile", 1899 "repo-local", 1900 "--instance", 1901 "primary", 1902 "--repo-local-root", 1903 root_path, 1904 "run", 1905 ]) 1906 .expect("test CLI"); 1907 let resolver = crate::RadrootsPathResolver::new( 1908 crate::RadrootsPlatform::Linux, 1909 crate::RadrootsHostEnvironment::default(), 1910 ); 1911 let runtime = crate::resolve_myc_runtime_context(&resolver, &invocation) 1912 .expect("test runtime context"); 1913 fs::create_dir_all(runtime.context().paths().run()).expect("runtime directory"); 1914 let configuration = 1915 crate::parse_myc_config_v1(CONFIG.as_bytes(), crate::MycConfigProfile::RepoLocal) 1916 .expect("test configuration"); 1917 (root, runtime, configuration) 1918 } 1919 1920 fn target_for(route: MycAdminRoute, request: &Value) -> AdminClientTarget { 1921 let path = route 1922 .path() 1923 .replace("{connection_id}", "connection-1") 1924 .replace("{challenge_id}", "challenge-1"); 1925 if !matches!(route.method(), MycAdminMethod::Get) 1926 || request.as_object().is_some_and(Map::is_empty) 1927 { 1928 return AdminClientTarget::new(path).expect("route target"); 1929 } 1930 let mut serializer = url::form_urlencoded::Serializer::new(String::new()); 1931 for (name, value) in request.as_object().expect("query object") { 1932 let rendered = value 1933 .as_str() 1934 .map(str::to_owned) 1935 .unwrap_or_else(|| value.to_string()); 1936 serializer.append_pair(name, &rendered); 1937 } 1938 AdminClientTarget::new(format!("{path}?{}", serializer.finish())).expect("query target") 1939 } 1940 1941 async fn raw_post(socket: &Path, body: &str) -> String { 1942 let mut stream = tokio::net::UnixStream::connect(socket) 1943 .await 1944 .expect("raw connection"); 1945 let request = format!( 1946 "POST /v1/discovery/render HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", 1947 body.len() 1948 ); 1949 stream 1950 .write_all(request.as_bytes()) 1951 .await 1952 .expect("raw request"); 1953 let mut response = Vec::new(); 1954 stream 1955 .read_to_end(&mut response) 1956 .await 1957 .expect("raw response"); 1958 String::from_utf8(response).expect("HTTP response") 1959 } 1960 1961 #[tokio::test] 1962 async fn all_nineteen_routes_round_trip_over_the_hardened_unix_boundary() { 1963 let (_root, runtime, configuration) = runtime_context(); 1964 let socket = runtime.artifacts().admin_socket().to_path_buf(); 1965 let handler = Arc::new(FixtureHandler::new()); 1966 let server = MycAdminServer::new(&configuration, Arc::clone(&handler)) 1967 .expect("production admin server") 1968 .bind(&runtime) 1969 .await 1970 .expect("canonical admin binding"); 1971 let cancellation = MycAdminCancellationToken::new(); 1972 let server_cancellation = cancellation.clone(); 1973 let task = tokio::spawn(async move { 1974 server 1975 .serve(server_cancellation) 1976 .await 1977 .expect("serve Myc admin"); 1978 }); 1979 let client = 1980 AdminClient::new(&socket, AdminTransportLimits::DEFAULT).expect("admin client"); 1981 1982 for (index, route) in MycAdminRoute::ALL.into_iter().enumerate() { 1983 let request = sample_model(route.request_model()); 1984 let target = target_for(route, &request); 1985 let response = match route.method() { 1986 MycAdminMethod::Get => client.get::<Value>(&target).await.expect("GET route"), 1987 MycAdminMethod::Post => { 1988 let operation_id = AdminOperationId::new(format!("operation-{index}")) 1989 .expect("operation ID"); 1990 client 1991 .mutate::<_, Value>(&target, operation_id, None, &request) 1992 .await 1993 .expect("POST route") 1994 } 1995 }; 1996 validate_model(route.response_model(), response.result()) 1997 .expect("route response model"); 1998 } 1999 2000 let cursor_target = 2001 AdminClientTarget::new("/v1/connections?cursor=eA&limit=1").expect("cursor target"); 2002 let cursor_error = client 2003 .get::<Value>(&cursor_target) 2004 .await 2005 .expect_err("handler rejects unbound cursor"); 2006 assert_eq!( 2007 cursor_error 2008 .failure() 2009 .expect("failure envelope") 2010 .error() 2011 .code() 2012 .as_str(), 2013 "invalid_cursor" 2014 ); 2015 assert_eq!(handler.invalid_cursor.load(Ordering::SeqCst), 1); 2016 2017 let conflict_target = 2018 AdminClientTarget::new("/v1/discovery/render").expect("mutation target"); 2019 let conflict_error = client 2020 .mutate::<_, Value>( 2021 &conflict_target, 2022 AdminOperationId::new("conflict").expect("operation ID"), 2023 None, 2024 sample_model("discovery_render_request_v1"), 2025 ) 2026 .await 2027 .expect_err("operation conflict"); 2028 assert_eq!( 2029 conflict_error 2030 .failure() 2031 .expect("failure envelope") 2032 .error() 2033 .code() 2034 .as_str(), 2035 "operation_id_conflict" 2036 ); 2037 2038 for body in [ 2039 r#"{"contract_version":1,"operation_id":"duplicate","request":{"expected_generation":0,"expected_generation":1}}"#, 2040 r#"{"contract_version":1,"operation_id":"null","request":{"expected_generation":null}}"#, 2041 ] { 2042 let response = raw_post(&socket, body).await; 2043 assert!(response.starts_with("HTTP/1.1 400 "), "{response}"); 2044 } 2045 2046 let encoded_target = 2047 AdminClientTarget::new("/v1/connections/connection%2Dencoded/approve") 2048 .expect("encoded parameter target"); 2049 client 2050 .mutate::<_, Value>( 2051 &encoded_target, 2052 AdminOperationId::new("encoded-parameter").expect("operation ID"), 2053 None, 2054 sample_model("connection_approve_request_v1"), 2055 ) 2056 .await 2057 .expect("percent-decoded path parameter"); 2058 assert_eq!(handler.decoded_parameter.load(Ordering::SeqCst), 1); 2059 2060 { 2061 let calls = handler.calls.lock().expect("calls"); 2062 assert_eq!(calls.len(), 19); 2063 for (index, (route, operation_id, request)) in calls.iter().enumerate() { 2064 assert_eq!(*route, MycAdminRoute::ALL[index]); 2065 assert_eq!(operation_id.is_some(), route.is_mutation()); 2066 validate_model( 2067 route.request_model(), 2068 &serde_json::from_slice(request).expect("retained request model"), 2069 ) 2070 .expect("retained exact model"); 2071 } 2072 } 2073 cancellation.cancel(); 2074 task.await.expect("server task"); 2075 assert!(!socket.exists()); 2076 } 2077 2078 #[test] 2079 fn production_server_projects_exact_validated_admin_limits() { 2080 let (_root, _runtime, configuration) = runtime_context(); 2081 assert_eq!( 2082 admin_transport_limits(&configuration).expect("admin limits"), 2083 AdminTransportLimits::DEFAULT 2084 ); 2085 } 2086 } 2087 }