1use crate::limits::*;
7use crate::*;
8
9fn validate_report_page_limit(
10 value: u32,
11 max: u32,
12 field: &'static str,
13) -> Result<(), WireValidationError> {
14 if value == 0 || value > max {
15 return Err(WireValidationError {
16 field,
17 message: format!("value is {value}, must be 1..={max}"),
18 });
19 }
20 Ok(())
21}
22
23impl WireValidate for RegisterPayload {
26 fn wire_validate(&self) -> Result<(), WireValidationError> {
27 Ok(())
28 }
29}
30
31impl WireValidate for ServiceMessage {
32 fn wire_validate(&self) -> Result<(), WireValidationError> {
33 match self {
34 ServiceMessage::Ping(_) => Ok(()),
35 ServiceMessage::Register(_) => Ok(()),
36 ServiceMessage::Enroll(p) => p.wire_validate(),
37 ServiceMessage::RequestCertificate(p) => p.wire_validate(),
38 ServiceMessage::RenewCertificate(p) => p.wire_validate(),
39 ServiceMessage::ReportHosts(p) => p.wire_validate(),
40 ServiceMessage::VersionCheckResults(p) => p.wire_validate(),
41 ServiceMessage::UpdateStarted(p) => p.wire_validate(),
42 ServiceMessage::UpdateOutput(p) => p.wire_validate(),
43 ServiceMessage::UpdateResult(p) => p.wire_validate(),
44 ServiceMessage::BatchUpdateResult(p) => p.wire_validate(),
45 ServiceMessage::DiscoveryResults(p) => p.wire_validate(),
46 ServiceMessage::StdinAttention(p) => p.wire_validate(),
47 ServiceMessage::ServiceTriggerUpdate(p) => p.wire_validate(),
48 ServiceMessage::ServiceTriggerHostBatchUpdate(_) => Ok(()),
49 ServiceMessage::Disconnecting(p) => p.wire_validate(),
50 ServiceMessage::ReportPluginConfig(p) => p.wire_validate(),
51 ServiceMessage::SurfaceRegistration(p) => p.wire_validate(),
52 ServiceMessage::SurfaceActionResponse(p) => p.wire_validate(),
53 ServiceMessage::SurfaceActionRequest(p) => p.wire_validate(),
54 ServiceMessage::StoreServiceConfig(p) => p.wire_validate(),
55 ServiceMessage::DeleteServiceConfig(p) => p.wire_validate(),
56 ServiceMessage::WorkloadClaim(p) => p.wire_validate(),
57 ServiceMessage::WorkloadRelease(p) => p.wire_validate(),
58 ServiceMessage::TestPluginConfigResult(p) => p.wire_validate(),
59 ServiceMessage::AuditEvent(_) => Ok(()),
60 _ => {
62 tracing::debug!(
63 "received unknown ServiceMessage variant from peer; skipping validation"
64 );
65 Ok(())
66 }
67 }
68 }
69}
70
71impl WireValidate for ControllerMessage {
74 fn wire_validate(&self) -> Result<(), WireValidationError> {
75 match self {
76 ControllerMessage::Pong(_) => Ok(()),
77 ControllerMessage::Enrolled(_) => Ok(()),
78 ControllerMessage::Approved(_) => Ok(()),
79 ControllerMessage::Rejected(_) => Ok(()),
80 ControllerMessage::Certificate(p) => p.wire_validate(),
81 ControllerMessage::Error(p) => p.wire_validate(),
82 ControllerMessage::ServiceSettings(p) => p.wire_validate(),
83 ControllerMessage::CaBundleUpdated(p) => p.wire_validate(),
84 ControllerMessage::RequestCertRenewal(p) => p.wire_validate(),
85 ControllerMessage::ServerRestarting(p) => p.wire_validate(),
86 ControllerMessage::CheckVersions(p) => p.wire_validate(),
87 ControllerMessage::ExecuteUpdate(p) => p.wire_validate(),
88 ControllerMessage::ExecuteBatchUpdate(p) => p.wire_validate(),
89 ControllerMessage::DiscoverSoftware(p) => p.wire_validate(),
90 ControllerMessage::SetUpdateFreeze(p) => p.wire_validate(),
91 ControllerMessage::UpdateStdinData(p) => p.wire_validate(),
92 ControllerMessage::SoftwareStates(p) => p.wire_validate(),
93 ControllerMessage::HostConnectivityUpdated(p) => p.wire_validate(),
94 ControllerMessage::ReportPluginConfigResponse(p) => p.wire_validate(),
95 ControllerMessage::SurfaceActionRequest(p) => p.wire_validate(),
96 ControllerMessage::SurfaceActionCancel(p) => p.wire_validate(),
97 ControllerMessage::SurfaceActionResponse(p) => p.wire_validate(),
98 ControllerMessage::ServiceCredentials(_) => Ok(()),
99 ControllerMessage::ServiceConfigDelivery(p) => p.wire_validate(),
100 ControllerMessage::ServiceConfigAck(p) => p.wire_validate(),
101 ControllerMessage::ServiceConfigUpdated(p) => p.wire_validate(),
102 ControllerMessage::RequestCaRotation(p) => p.wire_validate(),
103 ControllerMessage::RequestCrlRenewal(_) => Ok(()),
104 ControllerMessage::TokenRevoked(_) => Ok(()),
105 ControllerMessage::WorkloadClaimResult(p) => p.wire_validate(),
106 ControllerMessage::WorkloadClaimAnnouncement(p) => p.wire_validate(),
107 ControllerMessage::WorkloadClaimSyncRequest(_) => Ok(()),
108 ControllerMessage::WorkloadClaimSyncResponse(p) => p.wire_validate(),
109 ControllerMessage::TestPluginConfig(p) => p.wire_validate(),
110 _ => {
112 tracing::debug!(
113 "received unknown ControllerMessage variant from peer; skipping validation"
114 );
115 Ok(())
116 }
117 }
118 }
119}
120
121impl WireValidate for crate::envelope::ReportPagination {
124 fn wire_validate(&self) -> Result<(), WireValidationError> {
125 if self.total_pages == 0 || self.total_pages > MAX_REPORT_PAGES {
126 return Err(WireValidationError {
127 field: "pagination.total_pages",
128 message: format!(
129 "total_pages is {}, must be 1..={MAX_REPORT_PAGES}",
130 self.total_pages
131 ),
132 });
133 }
134 if self.page == 0 || self.page > self.total_pages {
135 return Err(WireValidationError {
136 field: "pagination.page",
137 message: format!("page is {}, must be 1..={}", self.page, self.total_pages),
138 });
139 }
140 Ok(())
141 }
142}
143
144impl WireValidate for EnrollPayload {
147 fn wire_validate(&self) -> Result<(), WireValidationError> {
148 check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
149 check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
150 check_string_len(
151 &self.service_app_name,
152 MAX_SHORT_STRING_LEN,
153 "service_app_name",
154 )?;
155 Ok(())
156 }
157}
158
159impl WireValidate for RequestCertificatePayload {
160 fn wire_validate(&self) -> Result<(), WireValidationError> {
161 check_string_len(&self.csr_pem, MAX_LONG_STRING_LEN, "csr_pem")?;
162 Ok(())
163 }
164}
165
166impl WireValidate for RenewCertificatePayload {
167 fn wire_validate(&self) -> Result<(), WireValidationError> {
168 check_string_len(&self.csr_pem, MAX_LONG_STRING_LEN, "csr_pem")?;
169 Ok(())
170 }
171}
172
173impl WireValidate for ReportHostsPayload {
174 fn wire_validate(&self) -> Result<(), WireValidationError> {
175 check_vec_len(&self.hosts, MAX_REPORT_HOSTS, "hosts")?;
176 check_string_len(&self.agent_version, MAX_SHORT_STRING_LEN, "agent_version")?;
177 for host in &self.hosts {
178 host.wire_validate()?;
179 }
180 Ok(())
181 }
182}
183
184impl WireValidate for HostInfo {
185 fn wire_validate(&self) -> Result<(), WireValidationError> {
186 check_string_len(&self.machine_id, MAX_SHORT_STRING_LEN, "machine_id")?;
187 check_opt_string_len(&self.os_type, MAX_SHORT_STRING_LEN, "os_type")?;
188 check_opt_string_len(&self.os_version, MAX_SHORT_STRING_LEN, "os_version")?;
189 check_opt_string_len(&self.architecture, MAX_SHORT_STRING_LEN, "architecture")?;
190 check_opt_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
191 check_opt_string_len(&self.ip_address, MAX_SHORT_STRING_LEN, "ip_address")?;
192 Ok(())
193 }
194}
195
196impl WireValidate for VersionCheckResultsPayload {
197 fn wire_validate(&self) -> Result<(), WireValidationError> {
198 check_vec_len(&self.results, MAX_VERSION_CHECK_RESULTS, "results")?;
199 for result in &self.results {
200 result.wire_validate()?;
201 }
202 Ok(())
203 }
204}
205
206impl WireValidate for VersionCheckResult {
207 fn wire_validate(&self) -> Result<(), WireValidationError> {
208 check_opt_string_len(
209 &self.installed_version,
210 MAX_SHORT_STRING_LEN,
211 "installed_version",
212 )?;
213 check_opt_string_len(&self.latest_version, MAX_SHORT_STRING_LEN, "latest_version")?;
214 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
215 Ok(())
216 }
217}
218
219impl WireValidate for UpdateStartedPayload {
220 fn wire_validate(&self) -> Result<(), WireValidationError> {
221 check_opt_string_len(&self.from_version, MAX_SHORT_STRING_LEN, "from_version")?;
222 Ok(())
223 }
224}
225
226impl WireValidate for UpdateOutputPayload {
227 fn wire_validate(&self) -> Result<(), WireValidationError> {
228 check_string_len(&self.output, MAX_OUTPUT_STRING_LEN, "output")?;
229 Ok(())
230 }
231}
232
233impl WireValidate for UpdateResultPayload {
234 fn wire_validate(&self) -> Result<(), WireValidationError> {
235 check_string_len(&self.output, MAX_OUTPUT_STRING_LEN, "output")?;
236 check_opt_string_len(&self.from_version, MAX_SHORT_STRING_LEN, "from_version")?;
237 check_opt_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
238 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
239 Ok(())
240 }
241}
242
243impl WireValidate for BatchUpdateResultPayload {
244 fn wire_validate(&self) -> Result<(), WireValidationError> {
245 check_vec_len(&self.results, MAX_BATCH_UPDATE_RESULTS, "results")?;
246 for result in &self.results {
247 result.wire_validate()?;
248 }
249 Ok(())
250 }
251}
252
253impl WireValidate for BatchUpdateItemResult {
254 fn wire_validate(&self) -> Result<(), WireValidationError> {
255 check_string_len(&self.output, MAX_OUTPUT_STRING_LEN, "output")?;
256 check_opt_string_len(
257 &self.installed_version,
258 MAX_SHORT_STRING_LEN,
259 "installed_version",
260 )?;
261 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
262 Ok(())
263 }
264}
265
266impl WireValidate for DiscoveryResultsPayload {
267 fn wire_validate(&self) -> Result<(), WireValidationError> {
268 check_string_len(
269 &self.host_machine_id,
270 MAX_SHORT_STRING_LEN,
271 "host_machine_id",
272 )?;
273 check_vec_len(&self.results, MAX_DISCOVERY_PLUGIN_RESULTS, "results")?;
274 for result in &self.results {
275 result.wire_validate()?;
276 }
277 Ok(())
278 }
279}
280
281impl WireValidate for DiscoveryPluginResult {
282 fn wire_validate(&self) -> Result<(), WireValidationError> {
283 check_vec_len(&self.discoveries, MAX_DISCOVERIES_PER_PLUGIN, "discoveries")?;
284 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
285 for discovery in &self.discoveries {
286 discovery.wire_validate()?;
287 }
288 Ok(())
289 }
290}
291
292impl WireValidate for uptrakit_shared_types::DiscoveredSoftware {
293 fn wire_validate(&self) -> Result<(), WireValidationError> {
294 check_string_len(
295 &self.package_identifier,
296 MAX_SHORT_STRING_LEN,
297 "package_identifier",
298 )?;
299 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
300 check_string_len(
301 &self.installed_version,
302 MAX_SHORT_STRING_LEN,
303 "installed_version",
304 )?;
305 check_opt_string_len(&self.qualifier, MAX_DISCOVERED_QUALIFIER_LEN, "qualifier")?;
306 check_opt_string_len(
307 &self.plugin_package_identifier,
308 MAX_SHORT_STRING_LEN,
309 "plugin_package_identifier",
310 )?;
311 Ok(())
312 }
313}
314
315impl WireValidate for ServiceUpdateTriggerPayload {
316 fn wire_validate(&self) -> Result<(), WireValidationError> {
317 check_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
318 Ok(())
319 }
320}
321
322impl WireValidate for DisconnectingPayload {
323 fn wire_validate(&self) -> Result<(), WireValidationError> {
324 Ok(())
325 }
326}
327
328fn validate_surface_json_bounds(
329 value: &serde_json::Value,
330 field: &'static str,
331) -> Result<(), WireValidationError> {
332 let mut node_count = 0usize;
333 fn walk(
334 value: &serde_json::Value,
335 depth: usize,
336 node_count: &mut usize,
337 field: &'static str,
338 ) -> Result<(), WireValidationError> {
339 if depth > MAX_SURFACE_JSON_DEPTH {
340 return Err(WireValidationError {
341 field,
342 message: format!(
343 "JSON depth exceeds max {MAX_SURFACE_JSON_DEPTH} (observed depth {depth})"
344 ),
345 });
346 }
347
348 *node_count += 1;
349 if *node_count > MAX_SURFACE_JSON_NODES {
350 return Err(WireValidationError {
351 field,
352 message: format!("JSON node count exceeds max {MAX_SURFACE_JSON_NODES}"),
353 });
354 }
355
356 match value {
357 serde_json::Value::Array(items) => {
358 for item in items {
359 walk(item, depth + 1, node_count, field)?;
360 }
361 }
362 serde_json::Value::Object(map) => {
363 for item in map.values() {
364 walk(item, depth + 1, node_count, field)?;
365 }
366 }
367 _ => {}
368 }
369
370 Ok(())
371 }
372
373 walk(value, 1, &mut node_count, field)
374}
375
376fn validate_surface_node(
377 node: &surfaces::SurfaceNode,
378 depth: usize,
379) -> Result<(), WireValidationError> {
380 if depth > MAX_SURFACE_JSON_DEPTH {
381 return Err(WireValidationError {
382 field: "surfaces[].descriptor.root_node",
383 message: format!(
384 "root node depth exceeds max {MAX_SURFACE_JSON_DEPTH} (observed depth {depth})"
385 ),
386 });
387 }
388
389 match node {
390 surfaces::SurfaceNode::Section { title, children } => {
391 check_opt_string_len(
392 title,
393 MAX_SHORT_STRING_LEN,
394 "surfaces[].descriptor.root_node.title",
395 )?;
396 check_vec_len(
397 children,
398 MAX_SURFACE_FIELDS,
399 "surfaces[].descriptor.root_node.children",
400 )?;
401 for child in children {
402 validate_surface_node(child, depth + 1)?;
403 }
404 }
405 surfaces::SurfaceNode::TextBlock { text } => {
406 check_string_len(
407 text,
408 MAX_MEDIUM_STRING_LEN,
409 "surfaces[].descriptor.root_node.text",
410 )?;
411 }
412 surfaces::SurfaceNode::KeyValue { .. } | surfaces::SurfaceNode::Table { .. } => {}
413 surfaces::SurfaceNode::Form { .. } => {}
414 surfaces::SurfaceNode::ActionBar { action_ids } => {
415 check_vec_len(
416 action_ids,
417 MAX_SURFACE_ACTION_REFS,
418 "surfaces[].descriptor.root_node.action_ids",
419 )?;
420 }
421 surfaces::SurfaceNode::Tabs { tabs } => {
422 check_vec_len(
423 tabs,
424 MAX_SURFACE_COLUMNS,
425 "surfaces[].descriptor.root_node.tabs",
426 )?;
427 for tab in tabs {
428 check_string_len(
429 &tab.label,
430 MAX_SHORT_STRING_LEN,
431 "surfaces[].descriptor.root_node.tabs[].label",
432 )?;
433 validate_surface_node(&tab.root, depth + 1)?;
434 }
435 }
436 surfaces::SurfaceNode::Callout { text, .. } => {
437 check_string_len(
438 text,
439 MAX_MEDIUM_STRING_LEN,
440 "surfaces[].descriptor.root_node.callout",
441 )?;
442 }
443 surfaces::SurfaceNode::EmptyState { title, description } => {
444 check_string_len(
445 title,
446 MAX_SHORT_STRING_LEN,
447 "surfaces[].descriptor.root_node.empty_state.title",
448 )?;
449 check_opt_string_len(
450 description,
451 MAX_MEDIUM_STRING_LEN,
452 "surfaces[].descriptor.root_node.empty_state.description",
453 )?;
454 }
455 surfaces::SurfaceNode::ModalTrigger { modal_nodes, .. } => {
456 check_vec_len(
457 modal_nodes,
458 MAX_SURFACE_FIELDS,
459 "surfaces[].descriptor.root_node.modal_nodes",
460 )?;
461 for child in modal_nodes {
462 validate_surface_node(child, depth + 1)?;
463 }
464 }
465 surfaces::SurfaceNode::WorkflowTrigger { step_nodes, .. } => {
466 check_vec_len(
467 step_nodes,
468 MAX_SURFACE_WIZARD_STEPS,
469 "surfaces[].descriptor.root_node.step_nodes",
470 )?;
471 for child in step_nodes {
472 validate_surface_node(child, depth + 1)?;
473 }
474 }
475 _ => {
476 tracing::warn!(
477 ?node,
478 "unknown SurfaceNode variant; skipping wire validation"
479 );
480 }
481 }
482
483 Ok(())
484}
485
486fn validate_surface_interaction(
487 interaction: &surfaces::InteractionDescriptor,
488) -> Result<(), WireValidationError> {
489 check_opt_string_len(
490 &interaction.required_permission,
491 MAX_SHORT_STRING_LEN,
492 "surfaces[].interactions[].required_permission",
493 )?;
494 check_vec_len(
495 &interaction.sensitive_fields,
496 MAX_SURFACE_FIELDS,
497 "surfaces[].interactions[].sensitive_fields",
498 )?;
499 for field in &interaction.sensitive_fields {
500 check_string_len(
501 field,
502 MAX_SHORT_STRING_LEN,
503 "surfaces[].interactions[].sensitive_fields[]",
504 )?;
505 }
506
507 if let Some(confirmation) = &interaction.confirmation {
508 check_string_len(
509 &confirmation.title,
510 MAX_SHORT_STRING_LEN,
511 "surfaces[].interactions[].confirmation.title",
512 )?;
513 check_string_len(
514 &confirmation.message,
515 MAX_MEDIUM_STRING_LEN,
516 "surfaces[].interactions[].confirmation.message",
517 )?;
518 check_opt_string_len(
519 &confirmation.confirm_label,
520 MAX_SHORT_STRING_LEN,
521 "surfaces[].interactions[].confirmation.confirm_label",
522 )?;
523 check_opt_string_len(
524 &confirmation.cancel_label,
525 MAX_SHORT_STRING_LEN,
526 "surfaces[].interactions[].confirmation.cancel_label",
527 )?;
528 }
529
530 check_vec_len(
531 &interaction.workflow_steps,
532 MAX_SURFACE_WIZARD_STEPS,
533 "surfaces[].interactions[].workflow_steps",
534 )?;
535 for step in &interaction.workflow_steps {
536 check_string_len(
537 &step.step_id,
538 MAX_SHORT_STRING_LEN,
539 "surfaces[].interactions[].workflow_steps[].step_id",
540 )?;
541 }
542
543 if let surfaces::InteractionTransport::DirectBuiltInApi { operation_id } =
544 &interaction.transport
545 {
546 check_string_len(
547 operation_id.as_str(),
548 MAX_SHORT_STRING_LEN,
549 "surfaces[].interactions[].transport.operation_id",
550 )?;
551 }
552
553 Ok(())
554}
555
556fn validate_surface_data_source(
557 data_source: &surfaces::DataSourceDescriptor,
558) -> Result<(), WireValidationError> {
559 match &data_source.kind {
560 surfaces::DataSourceKind::Static { data } => {
561 let data_len = serde_json::to_vec(data)
562 .map_err(|error| WireValidationError {
563 field: "surfaces[].data_sources[].kind.static.data",
564 message: format!("failed to serialize static data: {error}"),
565 })?
566 .len();
567 if data_len > MAX_SURFACE_PARAMS_LEN {
568 return Err(WireValidationError {
569 field: "surfaces[].data_sources[].kind.static.data",
570 message: format!(
571 "static data JSON is {data_len} bytes, max {MAX_SURFACE_PARAMS_LEN}"
572 ),
573 });
574 }
575 validate_surface_json_bounds(data, "surfaces[].data_sources[].kind.static.data")?;
576 }
577 surfaces::DataSourceKind::ControllerQuery { .. } => {}
578 surfaces::DataSourceKind::ProviderQuery { operation_id } => {
579 check_string_len(
580 operation_id,
581 MAX_SHORT_STRING_LEN,
582 "surfaces[].data_sources[].kind.provider_query.operation_id",
583 )?;
584 }
585 }
586
587 if let Some(pagination) = &data_source.pagination {
588 if pagination.default_page_size == 0 || pagination.max_page_size == 0 {
589 return Err(WireValidationError {
590 field: "surfaces[].data_sources[].pagination",
591 message: "page size values must be greater than zero".to_string(),
592 });
593 }
594 if pagination.default_page_size > pagination.max_page_size {
595 return Err(WireValidationError {
596 field: "surfaces[].data_sources[].pagination",
597 message: "default_page_size cannot exceed max_page_size".to_string(),
598 });
599 }
600 }
601
602 if let Some(sorting) = &data_source.sorting {
603 check_vec_len(
604 &sorting.sortable_fields,
605 MAX_SURFACE_COLUMNS,
606 "surfaces[].data_sources[].sorting.sortable_fields",
607 )?;
608 for field in &sorting.sortable_fields {
609 check_string_len(
610 field,
611 MAX_SHORT_STRING_LEN,
612 "surfaces[].data_sources[].sorting.sortable_fields[]",
613 )?;
614 }
615 check_opt_string_len(
616 &sorting.default_sort_field,
617 MAX_SHORT_STRING_LEN,
618 "surfaces[].data_sources[].sorting.default_sort_field",
619 )?;
620 }
621
622 if let Some(filtering) = &data_source.filtering {
623 check_vec_len(
624 &filtering.filter_fields,
625 MAX_SURFACE_COLUMNS,
626 "surfaces[].data_sources[].filtering.filter_fields",
627 )?;
628 for field in &filtering.filter_fields {
629 check_string_len(
630 field,
631 MAX_SHORT_STRING_LEN,
632 "surfaces[].data_sources[].filtering.filter_fields[]",
633 )?;
634 }
635 }
636
637 match &data_source.refresh_policy {
638 surfaces::RefreshPolicy::Manual => {}
639 surfaces::RefreshPolicy::Interval { seconds } => {
640 if *seconds == 0 {
641 return Err(WireValidationError {
642 field: "surfaces[].data_sources[].refresh_policy.interval.seconds",
643 message: "interval seconds must be greater than zero".to_string(),
644 });
645 }
646 }
647 surfaces::RefreshPolicy::Sse { .. } => {}
648 }
649
650 if let Some(empty_state) = &data_source.empty_state {
651 check_string_len(
652 &empty_state.title,
653 MAX_SHORT_STRING_LEN,
654 "surfaces[].data_sources[].empty_state.title",
655 )?;
656 check_opt_string_len(
657 &empty_state.description,
658 MAX_MEDIUM_STRING_LEN,
659 "surfaces[].data_sources[].empty_state.description",
660 )?;
661 }
662
663 Ok(())
664}
665
666impl WireValidate for surfaces::SurfaceRegistration {
667 fn wire_validate(&self) -> Result<(), WireValidationError> {
668 check_string_len(
669 &self.provider.provider_id,
670 MAX_SHORT_STRING_LEN,
671 "provider.provider_id",
672 )?;
673 check_string_len(
674 &self.provider.provider_namespace,
675 MAX_SHORT_STRING_LEN,
676 "provider.provider_namespace",
677 )?;
678 check_opt_string_len(
679 &self.effective_tenant_binding.tenant_id,
680 MAX_SHORT_STRING_LEN,
681 "effective_tenant_binding.tenant_id",
682 )?;
683 if self.effective_tenant_binding.scope == surfaces::Scope::Tenant {
684 let tenant_id =
685 self.effective_tenant_binding
686 .tenant_id
687 .as_deref()
688 .ok_or(WireValidationError {
689 field: "effective_tenant_binding.tenant_id",
690 message: "tenant scope requires tenant_id".to_string(),
691 })?;
692 uuid::Uuid::parse_str(tenant_id).map_err(|error| WireValidationError {
693 field: "effective_tenant_binding.tenant_id",
694 message: format!("invalid tenant UUID: {error}"),
695 })?;
696 } else if let Some(tenant_id) = &self.effective_tenant_binding.tenant_id {
697 uuid::Uuid::parse_str(tenant_id).map_err(|error| WireValidationError {
698 field: "effective_tenant_binding.tenant_id",
699 message: format!("invalid tenant UUID: {error}"),
700 })?;
701 }
702 check_vec_len(&self.surfaces, MAX_SURFACE_MANIFESTS, "surfaces")?;
703
704 if let Some(ref metadata) = self.encryption_metadata {
705 check_string_len(
706 &metadata.key_id,
707 MAX_SHORT_STRING_LEN,
708 "encryption_metadata.key_id",
709 )?;
710 check_string_len(
711 &metadata.public_key,
712 MAX_LONG_STRING_LEN,
713 "encryption_metadata.public_key",
714 )?;
715 }
716
717 for surface in &self.surfaces {
718 check_string_len(
719 &surface.descriptor.label,
720 MAX_SHORT_STRING_LEN,
721 "surfaces[].descriptor.label",
722 )?;
723 check_string_len(
724 &surface.descriptor.slot,
725 MAX_SHORT_STRING_LEN,
726 "surfaces[].descriptor.slot",
727 )?;
728 check_opt_string_len(
729 &surface.descriptor.required_permission,
730 MAX_SHORT_STRING_LEN,
731 "surfaces[].descriptor.required_permission",
732 )?;
733 check_vec_len(
734 &surface.interactions,
735 MAX_SURFACE_ACTIONS,
736 "surfaces[].interactions",
737 )?;
738 check_vec_len(
739 &surface.data_sources,
740 MAX_SURFACE_FIELDS,
741 "surfaces[].data_sources",
742 )?;
743 validate_surface_node(&surface.descriptor.root_node, 1)?;
744 for interaction in &surface.interactions {
745 validate_surface_interaction(interaction)?;
746 }
747 for data_source in &surface.data_sources {
748 validate_surface_data_source(data_source)?;
749 }
750 }
751
752 Ok(())
753 }
754}
755
756impl WireValidate for surfaces::SurfaceActionRequest {
757 fn wire_validate(&self) -> Result<(), WireValidationError> {
758 check_string_len(&self.tenant_id, MAX_SHORT_STRING_LEN, "tenant_id")?;
759 uuid::Uuid::parse_str(&self.tenant_id).map_err(|error| WireValidationError {
760 field: "tenant_id",
761 message: format!("invalid tenant UUID: {error}"),
762 })?;
763 check_string_len(
764 &self.idempotency_key,
765 MAX_SHORT_STRING_LEN,
766 "idempotency_key",
767 )?;
768 check_opt_string_len(
769 &self.target_provider_id,
770 MAX_SHORT_STRING_LEN,
771 "target_provider_id",
772 )?;
773
774 match &self.caller_origin {
775 surfaces::CallerOrigin::UserSession {
776 user_id,
777 session_id,
778 } => {
779 check_string_len(user_id, MAX_SHORT_STRING_LEN, "caller_origin.user_id")?;
780 check_string_len(session_id, MAX_SHORT_STRING_LEN, "caller_origin.session_id")?;
781 }
782 surfaces::CallerOrigin::BuiltInSystem { principal } => {
783 check_string_len(principal, MAX_SHORT_STRING_LEN, "caller_origin.principal")?;
784 }
785 surfaces::CallerOrigin::Provider { provider_id } => {
786 check_string_len(
787 provider_id,
788 MAX_SHORT_STRING_LEN,
789 "caller_origin.provider_id",
790 )?;
791 }
792 }
793
794 let params_len = serde_json::to_vec(&self.params)
795 .map_err(|error| WireValidationError {
796 field: "params",
797 message: format!("failed to serialize params: {error}"),
798 })?
799 .len();
800 if params_len > MAX_SURFACE_PARAMS_LEN {
801 return Err(WireValidationError {
802 field: "params",
803 message: format!("params JSON is {params_len} bytes, max {MAX_SURFACE_PARAMS_LEN}"),
804 });
805 }
806 validate_surface_json_bounds(&serde_json::Value::Object(self.params.clone()), "params")?;
807
808 if let Some(ref encrypted) = self.encrypted_sensitive_params {
809 check_string_len(
810 &encrypted.key_id,
811 MAX_SHORT_STRING_LEN,
812 "encrypted_sensitive_params.key_id",
813 )?;
814 check_string_len(
815 &encrypted.ciphertext_b64,
816 MAX_LONG_STRING_LEN,
817 "encrypted_sensitive_params.ciphertext_b64",
818 )?;
819 }
820
821 Ok(())
822 }
823}
824
825impl WireValidate for surfaces::SurfaceActionCancel {
826 fn wire_validate(&self) -> Result<(), WireValidationError> {
827 check_string_len(
828 &self.target_provider_id,
829 MAX_SHORT_STRING_LEN,
830 "target_provider_id",
831 )?;
832 Ok(())
833 }
834}
835
836impl WireValidate for surfaces::SurfaceActionResponse {
837 fn wire_validate(&self) -> Result<(), WireValidationError> {
838 if let Some(ref result) = self.result {
839 let result_len = serde_json::to_vec(result)
840 .map_err(|error| WireValidationError {
841 field: "result",
842 message: format!("failed to serialize result: {error}"),
843 })?
844 .len();
845 if result_len > MAX_SURFACE_RESPONSE_LEN {
846 return Err(WireValidationError {
847 field: "result",
848 message: format!(
849 "response result is {result_len} bytes, max {MAX_SURFACE_RESPONSE_LEN}"
850 ),
851 });
852 }
853 validate_surface_json_bounds(result, "result")?;
854 }
855
856 if let Some(ref error) = self.error {
857 error.wire_validate()?;
858 }
859
860 Ok(())
861 }
862}
863
864impl WireValidate for surfaces::SurfaceActionError {
865 fn wire_validate(&self) -> Result<(), WireValidationError> {
866 check_string_len(&self.message, MAX_MEDIUM_STRING_LEN, "error.message")?;
867
868 if let Some(ref details) = self.details {
869 let details_len = serde_json::to_vec(details)
870 .map_err(|error| WireValidationError {
871 field: "error.details",
872 message: format!("failed to serialize details: {error}"),
873 })?
874 .len();
875 if details_len > MAX_SURFACE_RESPONSE_LEN {
876 return Err(WireValidationError {
877 field: "error.details",
878 message: format!(
879 "error details are {details_len} bytes, max {MAX_SURFACE_RESPONSE_LEN}"
880 ),
881 });
882 }
883 validate_surface_json_bounds(details, "error.details")?;
884 }
885
886 Ok(())
887 }
888}
889
890impl WireValidate for ReportPluginConfigPayload {
891 fn wire_validate(&self) -> Result<(), WireValidationError> {
892 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
893 check_string_len(&self.plugin_type, MAX_SHORT_STRING_LEN, "plugin_type")?;
894 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
895 let config_str = self.config.to_string();
896 check_string_len(&config_str, MAX_PLUGIN_CONFIG_JSON_LEN, "config")?;
897 Ok(())
898 }
899}
900
901impl WireValidate for ReportPluginConfigResponsePayload {
904 fn wire_validate(&self) -> Result<(), WireValidationError> {
905 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
906 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
907 Ok(())
908 }
909}
910
911impl WireValidate for CertificatePayload {
912 fn wire_validate(&self) -> Result<(), WireValidationError> {
913 check_string_len(&self.cert_pem, MAX_LONG_STRING_LEN, "cert_pem")?;
914 Ok(())
915 }
916}
917
918impl WireValidate for ErrorPayload {
919 fn wire_validate(&self) -> Result<(), WireValidationError> {
920 check_string_len(&self.message, MAX_MEDIUM_STRING_LEN, "message")?;
921 Ok(())
922 }
923}
924
925impl WireValidate for ServiceSettingsPayload {
926 fn wire_validate(&self) -> Result<(), WireValidationError> {
927 check_string_len(&self.ca_bundle_hash, MAX_SHORT_STRING_LEN, "ca_bundle_hash")?;
928 self.report_page_limits.wire_validate()?;
929 Ok(())
930 }
931}
932
933impl WireValidate for ReportPageLimits {
934 fn wire_validate(&self) -> Result<(), WireValidationError> {
935 validate_report_page_limit(
936 self.report_hosts,
937 MAX_REPORT_HOSTS as u32,
938 "report_page_limits.report_hosts",
939 )?;
940 validate_report_page_limit(
941 self.version_check_results,
942 MAX_VERSION_CHECK_RESULTS as u32,
943 "report_page_limits.version_check_results",
944 )?;
945 validate_report_page_limit(
946 self.discovery_results,
947 MAX_DISCOVERY_PLUGIN_RESULTS as u32,
948 "report_page_limits.discovery_results",
949 )?;
950 validate_report_page_limit(
951 self.batch_update_results,
952 MAX_BATCH_UPDATE_RESULTS as u32,
953 "report_page_limits.batch_update_results",
954 )?;
955 Ok(())
956 }
957}
958
959impl WireValidate for CaBundleUpdatedPayload {
960 fn wire_validate(&self) -> Result<(), WireValidationError> {
961 check_string_len(&self.ca_bundle_pem, MAX_LONG_STRING_LEN, "ca_bundle_pem")?;
962 Ok(())
963 }
964}
965
966impl WireValidate for RequestCertRenewalPayload {
967 fn wire_validate(&self) -> Result<(), WireValidationError> {
968 check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
969 Ok(())
970 }
971}
972
973impl WireValidate for ServerRestartingPayload {
974 fn wire_validate(&self) -> Result<(), WireValidationError> {
975 check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
976 Ok(())
977 }
978}
979
980impl WireValidate for CheckVersionsPayload {
981 fn wire_validate(&self) -> Result<(), WireValidationError> {
982 check_string_len(
983 &self.host_machine_id,
984 MAX_SHORT_STRING_LEN,
985 "host_machine_id",
986 )?;
987 check_vec_len(
988 &self.assignments,
989 MAX_VERSION_CHECK_ASSIGNMENTS,
990 "assignments",
991 )?;
992 for assignment in &self.assignments {
993 assignment.wire_validate()?;
994 }
995 Ok(())
996 }
997}
998
999impl WireValidate for VersionCheckAssignment {
1000 fn wire_validate(&self) -> Result<(), WireValidationError> {
1001 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
1002 if let Some(ref pa) = self.detect_version {
1003 pa.wire_validate()?;
1004 }
1005 if let Some(ref pa) = self.fetch_releases {
1006 pa.wire_validate()?;
1007 }
1008 Ok(())
1009 }
1010}
1011
1012impl WireValidate for PluginAssignment {
1013 fn wire_validate(&self) -> Result<(), WireValidationError> {
1014 check_string_len(
1015 &self.package_identifier,
1016 MAX_SHORT_STRING_LEN,
1017 "package_identifier",
1018 )?;
1019 Ok(())
1020 }
1021}
1022
1023impl WireValidate for ReleaseAsset {
1024 fn wire_validate(&self) -> Result<(), WireValidationError> {
1025 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "asset.name")?;
1026 check_string_len(
1027 &self.download_url,
1028 MAX_MEDIUM_STRING_LEN,
1029 "asset.download_url",
1030 )?;
1031 if let Some(ref d) = self.sha256_digest
1032 && (d.len() != SHA256_DIGEST_LEN || !d.chars().all(|c| c.is_ascii_hexdigit()))
1033 {
1034 return Err(WireValidationError {
1035 field: "asset.sha256_digest",
1036 message: format!("expected {SHA256_DIGEST_LEN} hex chars, got {}", d.len()),
1037 });
1038 }
1039 Ok(())
1040 }
1041}
1042
1043impl WireValidate for ReleaseInfo {
1044 fn wire_validate(&self) -> Result<(), WireValidationError> {
1045 check_string_len(&self.tag, MAX_SHORT_STRING_LEN, "release_info.tag")?;
1046 check_string_len(
1047 &self.release_url,
1048 MAX_MEDIUM_STRING_LEN,
1049 "release_info.release_url",
1050 )?;
1051 check_vec_len(&self.assets, MAX_RELEASE_ASSETS, "release_info.assets")?;
1052 for asset in &self.assets {
1053 asset.wire_validate()?;
1054 }
1055 Ok(())
1056 }
1057}
1058
1059impl WireValidate for ExecuteUpdatePayload {
1060 fn wire_validate(&self) -> Result<(), WireValidationError> {
1061 check_string_len(
1062 &self.host_machine_id,
1063 MAX_SHORT_STRING_LEN,
1064 "host_machine_id",
1065 )?;
1066 check_string_len(
1067 &self.software_item_name,
1068 MAX_SHORT_STRING_LEN,
1069 "software_item_name",
1070 )?;
1071 check_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
1072 check_vec_len(
1073 &self.pre_update_hook_plugins,
1074 MAX_UPDATE_HOOKS,
1075 "pre_update_hook_plugins",
1076 )?;
1077 check_vec_len(
1078 &self.post_update_hook_plugins,
1079 MAX_UPDATE_HOOKS,
1080 "post_update_hook_plugins",
1081 )?;
1082 self.execute_update_plugin.wire_validate()?;
1083 if let Some(ref detect) = self.detect_version_plugin {
1084 detect.wire_validate()?;
1085 }
1086 if let Some(ref ri) = self.release_info {
1087 ri.wire_validate()?;
1088 }
1089 for plugin in &self.pre_update_hook_plugins {
1090 plugin.wire_validate()?;
1091 }
1092 for plugin in &self.post_update_hook_plugins {
1093 plugin.wire_validate()?;
1094 }
1095 Ok(())
1096 }
1097}
1098
1099impl WireValidate for ExecuteBatchUpdatePayload {
1100 fn wire_validate(&self) -> Result<(), WireValidationError> {
1101 check_string_len(
1102 &self.host_machine_id,
1103 MAX_SHORT_STRING_LEN,
1104 "host_machine_id",
1105 )?;
1106 check_vec_len(&self.updates, MAX_BATCH_UPDATES, "updates")?;
1107 check_vec_len(
1108 &self.pre_update_hook_plugins,
1109 MAX_UPDATE_HOOKS,
1110 "pre_update_hook_plugins",
1111 )?;
1112 check_vec_len(
1113 &self.post_update_hook_plugins,
1114 MAX_UPDATE_HOOKS,
1115 "post_update_hook_plugins",
1116 )?;
1117 for update in &self.updates {
1118 update.wire_validate()?;
1119 }
1120 for plugin in &self.pre_update_hook_plugins {
1121 plugin.wire_validate()?;
1122 }
1123 for plugin in &self.post_update_hook_plugins {
1124 plugin.wire_validate()?;
1125 }
1126 Ok(())
1127 }
1128}
1129
1130impl WireValidate for BatchUpdateItem {
1131 fn wire_validate(&self) -> Result<(), WireValidationError> {
1132 check_string_len(
1133 &self.package_identifier,
1134 MAX_SHORT_STRING_LEN,
1135 "package_identifier",
1136 )?;
1137 check_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
1138 Ok(())
1139 }
1140}
1141
1142impl WireValidate for DiscoverSoftwarePayload {
1143 fn wire_validate(&self) -> Result<(), WireValidationError> {
1144 check_string_len(
1145 &self.host_machine_id,
1146 MAX_SHORT_STRING_LEN,
1147 "host_machine_id",
1148 )?;
1149 check_vec_len(&self.plugins, MAX_DISCOVERY_PLUGINS, "plugins")?;
1150 Ok(())
1151 }
1152}
1153
1154impl WireValidate for SetUpdateFreezePayload {
1155 fn wire_validate(&self) -> Result<(), WireValidationError> {
1156 check_opt_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
1157 Ok(())
1158 }
1159}
1160
1161impl WireValidate for UpdateStdinDataPayload {
1162 fn wire_validate(&self) -> Result<(), WireValidationError> {
1163 check_string_len(&self.data, MAX_STDIN_DATA_LEN, "data")?;
1164 Ok(())
1165 }
1166}
1167
1168impl WireValidate for StdinAttentionPayload {
1169 fn wire_validate(&self) -> Result<(), WireValidationError> {
1170 check_opt_string_len(&self.hint, MAX_MEDIUM_STRING_LEN, "hint")?;
1171 Ok(())
1172 }
1173}
1174
1175impl WireValidate for SoftwareStatesPayload {
1176 fn wire_validate(&self) -> Result<(), WireValidationError> {
1177 if self.page.total_pages < 1 {
1178 return Err(WireValidationError {
1179 field: "page.total_pages",
1180 message: "total_pages must be at least 1".to_string(),
1181 });
1182 }
1183 if self.page.page_index >= self.page.total_pages {
1184 return Err(WireValidationError {
1185 field: "page.page_index",
1186 message: format!(
1187 "page_index {} must be less than total_pages {}",
1188 self.page.page_index, self.page.total_pages
1189 ),
1190 });
1191 }
1192 check_vec_len(&self.items, MAX_SOFTWARE_STATE_ITEMS, "items")?;
1193 check_vec_len(
1194 &self.host_summaries,
1195 MAX_HOST_PACKAGE_HOST_STATES,
1196 "host_summaries",
1197 )?;
1198 check_vec_len(&self.hosts, MAX_MQTT_HOSTS, "hosts")?;
1199 for item in &self.items {
1200 item.wire_validate()?;
1201 }
1202 for host_state in &self.host_summaries {
1203 host_state.wire_validate()?;
1204 }
1205 for host in &self.hosts {
1206 host.wire_validate()?;
1207 }
1208 Ok(())
1209 }
1210}
1211
1212impl WireValidate for SoftwareStateItem {
1213 fn wire_validate(&self) -> Result<(), WireValidationError> {
1214 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
1215 check_opt_string_len(&self.icon_url, MAX_ICON_URL_LEN, "icon_url")?;
1216 check_vec_len(&self.hosts, MAX_SOFTWARE_STATE_HOSTS, "hosts")?;
1217 for host in &self.hosts {
1218 host.wire_validate()?;
1219 }
1220 Ok(())
1221 }
1222}
1223
1224impl WireValidate for SoftwareStateHostEntry {
1225 fn wire_validate(&self) -> Result<(), WireValidationError> {
1226 check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1227 check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1228 check_opt_string_len(
1229 &self.installed_version,
1230 MAX_SHORT_STRING_LEN,
1231 "installed_version",
1232 )?;
1233 check_opt_string_len(&self.latest_version, MAX_SHORT_STRING_LEN, "latest_version")?;
1234 check_opt_string_len(&self.release_url, MAX_MEDIUM_STRING_LEN, "release_url")?;
1235 check_opt_string_len(&self.release_notes, MAX_LONG_STRING_LEN, "release_notes")?;
1236 check_opt_string_len(
1237 &self.update_category,
1238 MAX_SHORT_STRING_LEN,
1239 "update_category",
1240 )?;
1241 check_opt_string_len(&self.release_date, MAX_SHORT_STRING_LEN, "release_date")?;
1242 check_opt_string_len(
1243 &self.last_checked_at,
1244 MAX_SHORT_STRING_LEN,
1245 "last_checked_at",
1246 )?;
1247 Ok(())
1248 }
1249}
1250
1251impl WireValidate for HostPackageSummary {
1252 fn wire_validate(&self) -> Result<(), WireValidationError> {
1253 check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1254 check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1255 Ok(())
1256 }
1257}
1258
1259impl WireValidate for HostStateMetadata {
1260 fn wire_validate(&self) -> Result<(), WireValidationError> {
1261 check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1262 check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1263 check_opt_string_len(&self.os_type, MAX_SHORT_STRING_LEN, "os_type")?;
1264 check_opt_string_len(&self.os_version, MAX_SHORT_STRING_LEN, "os_version")?;
1265 check_opt_string_len(&self.architecture, MAX_SHORT_STRING_LEN, "architecture")?;
1266 check_vec_len(&self.tags, MAX_HOST_TAGS, "tags")?;
1267 for tag in &self.tags {
1268 check_string_len(tag, MAX_SHORT_STRING_LEN, "tags[]")?;
1269 }
1270 check_opt_string_len(&self.agent_version, MAX_SHORT_STRING_LEN, "agent_version")?;
1271 check_opt_string_len(
1272 &self.agent_last_seen_at,
1273 MAX_SHORT_STRING_LEN,
1274 "agent_last_seen_at",
1275 )?;
1276 Ok(())
1277 }
1278}
1279
1280impl WireValidate for HostConnectivityUpdatedPayload {
1281 fn wire_validate(&self) -> Result<(), WireValidationError> {
1282 check_vec_len(&self.updates, MAX_CONNECTIVITY_UPDATES, "updates")?;
1283 for update in &self.updates {
1284 check_opt_string_len(&update.last_seen_at, MAX_SHORT_STRING_LEN, "last_seen_at")?;
1285 check_opt_string_len(&update.agent_version, MAX_SHORT_STRING_LEN, "agent_version")?;
1286 }
1287 Ok(())
1288 }
1289}
1290
1291impl WireValidate for RequestCaRotationPayload {
1292 fn wire_validate(&self) -> Result<(), WireValidationError> {
1293 check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
1294 Ok(())
1295 }
1296}
1297
1298impl WireValidate for StoreServiceConfigPayload {
1301 fn wire_validate(&self) -> Result<(), WireValidationError> {
1302 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1303 check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1304 let value_str = self.value.to_string();
1305 check_string_len(&value_str, MAX_SERVICE_CONFIG_VALUE_LEN, "value")?;
1306 Ok(())
1307 }
1308}
1309
1310impl WireValidate for DeleteServiceConfigPayload {
1311 fn wire_validate(&self) -> Result<(), WireValidationError> {
1312 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1313 check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1314 Ok(())
1315 }
1316}
1317
1318impl WireValidate for ServiceConfigAckPayload {
1319 fn wire_validate(&self) -> Result<(), WireValidationError> {
1320 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1321 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
1322 Ok(())
1323 }
1324}
1325
1326impl WireValidate for ServiceConfigEntry {
1327 fn wire_validate(&self) -> Result<(), WireValidationError> {
1328 check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1329 let value_str = self.value.to_string();
1330 check_string_len(&value_str, MAX_SERVICE_CONFIG_VALUE_LEN, "value")?;
1331 Ok(())
1332 }
1333}
1334
1335impl WireValidate for ServiceConfigKey {
1336 fn wire_validate(&self) -> Result<(), WireValidationError> {
1337 check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1338 Ok(())
1339 }
1340}
1341
1342impl WireValidate for ServiceConfigDeliveryPayload {
1343 fn wire_validate(&self) -> Result<(), WireValidationError> {
1344 check_vec_len(&self.entries, MAX_SERVICE_CONFIG_ENTRIES, "entries")?;
1345 for (i, entry) in self.entries.iter().enumerate() {
1346 entry.wire_validate().map_err(|mut e| {
1347 e.field = "entries[i]";
1348 e
1349 })?;
1350 let _ = i; }
1352 Ok(())
1353 }
1354}
1355
1356impl WireValidate for ServiceConfigUpdatedPayload {
1357 fn wire_validate(&self) -> Result<(), WireValidationError> {
1358 check_vec_len(&self.changed, MAX_SERVICE_CONFIG_ENTRIES, "changed")?;
1359 check_vec_len(&self.deleted, MAX_SERVICE_CONFIG_ENTRIES, "deleted")?;
1360 for entry in &self.changed {
1361 entry.wire_validate()?;
1362 }
1363 for key in &self.deleted {
1364 key.wire_validate()?;
1365 }
1366 Ok(())
1367 }
1368}
1369
1370impl WireValidate for WorkloadClaimPayload {
1373 fn wire_validate(&self) -> Result<(), WireValidationError> {
1374 check_map_len(&self.claims, MAX_WORKLOAD_CLAIM_KEYS, "claims")?;
1375 for key in self.claims.keys() {
1376 check_string_len(key, MAX_SHORT_STRING_LEN, "claims[key]")?;
1377 }
1378 Ok(())
1379 }
1380}
1381
1382impl WireValidate for WorkloadClaimResultPayload {
1383 fn wire_validate(&self) -> Result<(), WireValidationError> {
1384 check_set_len(&self.granted, MAX_WORKLOAD_CLAIM_KEYS, "granted")?;
1385 check_set_len(&self.rejected, MAX_WORKLOAD_CLAIM_KEYS, "rejected")?;
1386 for key in &self.granted {
1387 check_string_len(key, MAX_SHORT_STRING_LEN, "granted[key]")?;
1388 }
1389 for key in &self.rejected {
1390 check_string_len(key, MAX_SHORT_STRING_LEN, "rejected[key]")?;
1391 }
1392 Ok(())
1393 }
1394}
1395
1396impl WireValidate for WorkloadReleasePayload {
1397 fn wire_validate(&self) -> Result<(), WireValidationError> {
1398 check_set_len(&self.keys, MAX_WORKLOAD_CLAIM_KEYS, "keys")?;
1399 for key in &self.keys {
1400 check_string_len(key, MAX_SHORT_STRING_LEN, "keys[key]")?;
1401 }
1402 Ok(())
1403 }
1404}
1405
1406impl WireValidate for WorkloadClaimAnnouncementPayload {
1407 fn wire_validate(&self) -> Result<(), WireValidationError> {
1408 check_map_len(&self.claimed, MAX_WORKLOAD_CLAIM_KEYS, "claimed")?;
1409 check_set_len(&self.released, MAX_WORKLOAD_CLAIM_KEYS, "released")?;
1410 check_string_len(&self.claimed_at, MAX_SHORT_STRING_LEN, "claimed_at")?;
1411 for key in self.claimed.keys() {
1412 check_string_len(key, MAX_SHORT_STRING_LEN, "claimed[key]")?;
1413 }
1414 for key in &self.released {
1415 check_string_len(key, MAX_SHORT_STRING_LEN, "released[key]")?;
1416 }
1417 Ok(())
1418 }
1419}
1420
1421impl WireValidate for WorkloadClaimSyncResponsePayload {
1422 fn wire_validate(&self) -> Result<(), WireValidationError> {
1423 check_map_len(&self.claims, MAX_WORKLOAD_CLAIM_KEYS, "claims")?;
1424 for (key, entry) in &self.claims {
1425 check_string_len(key, MAX_SHORT_STRING_LEN, "claims[key]")?;
1426 check_string_len(
1427 &entry.claimed_at,
1428 MAX_SHORT_STRING_LEN,
1429 "claims[].claimed_at",
1430 )?;
1431 }
1432 Ok(())
1433 }
1434}
1435
1436impl WireValidate for TestPluginConfigPayload {
1439 fn wire_validate(&self) -> Result<(), WireValidationError> {
1440 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1441 check_string_len(
1442 &self.host_machine_id,
1443 MAX_SHORT_STRING_LEN,
1444 "host_machine_id",
1445 )?;
1446 check_string_len(&self.plugin_type, MAX_SHORT_STRING_LEN, "plugin_type")?;
1447 check_opt_string_len(
1448 &self.package_identifier,
1449 MAX_SHORT_STRING_LEN,
1450 "package_identifier",
1451 )?;
1452 let config_str = self.config.to_string();
1453 check_string_len(&config_str, MAX_PLUGIN_CONFIG_JSON_LEN, "config")?;
1454 Ok(())
1455 }
1456}
1457
1458impl WireValidate for TestPluginConfigResultPayload {
1459 fn wire_validate(&self) -> Result<(), WireValidationError> {
1460 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1461 check_opt_string_len(&self.output, MAX_CONFIG_TEST_OUTPUT_LEN, "output")?;
1462 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
1463 check_opt_string_len(
1464 &self.detected_version,
1465 MAX_SHORT_STRING_LEN,
1466 "detected_version",
1467 )?;
1468 Ok(())
1469 }
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474 use super::*;
1475
1476 #[test]
1477 fn service_message_report_hosts_validates() {
1478 let msg = ServiceMessage::ReportHosts(ReportHostsPayload {
1479 hosts: vec![HostInfo {
1480 machine_id: "test-id".to_string(),
1481 os_type: Some("linux".to_string()),
1482 os_version: None,
1483 architecture: None,
1484 hostname: None,
1485 ip_address: None,
1486 agent_host_id: None,
1487 features: None,
1488 }],
1489 agent_version: "1.0.0".to_string(),
1490 capabilities: std::collections::BTreeSet::new(),
1491 });
1492 assert!(msg.wire_validate().is_ok());
1493 }
1494
1495 #[test]
1496 fn service_message_report_hosts_too_many() {
1497 let hosts: Vec<HostInfo> = (0..MAX_REPORT_HOSTS + 1)
1498 .map(|i| HostInfo {
1499 machine_id: format!("host-{i}"),
1500 os_type: None,
1501 os_version: None,
1502 architecture: None,
1503 hostname: None,
1504 ip_address: None,
1505 agent_host_id: None,
1506 features: None,
1507 })
1508 .collect();
1509 let msg = ServiceMessage::ReportHosts(ReportHostsPayload {
1510 hosts,
1511 agent_version: "1.0.0".to_string(),
1512 capabilities: std::collections::BTreeSet::new(),
1513 });
1514 let err = msg.wire_validate().unwrap_err();
1515 assert_eq!(err.field, "hosts");
1516 }
1517
1518 #[test]
1519 fn controller_message_check_versions_validates() {
1520 let msg = ControllerMessage::CheckVersions(CheckVersionsPayload {
1521 host_machine_id: "test".to_string(),
1522 assignments: vec![],
1523 });
1524 assert!(msg.wire_validate().is_ok());
1525 }
1526
1527 #[test]
1528 fn controller_message_check_versions_too_many() {
1529 let assignments: Vec<VersionCheckAssignment> = (0..MAX_VERSION_CHECK_ASSIGNMENTS + 1)
1530 .map(|i| VersionCheckAssignment {
1531 software_item_id: uuid::Uuid::nil(),
1532 name: format!("item-{i}"),
1533 detect_version: None,
1534 fetch_releases: None,
1535 host_software_item_id: None,
1536 })
1537 .collect();
1538 let msg = ControllerMessage::CheckVersions(CheckVersionsPayload {
1539 host_machine_id: "test".to_string(),
1540 assignments,
1541 });
1542 let err = msg.wire_validate().unwrap_err();
1543 assert_eq!(err.field, "assignments");
1544 }
1545
1546 #[test]
1547 fn set_update_freeze_validates() {
1548 let payload = SetUpdateFreezePayload {
1549 enabled: true,
1550 reason: Some("test".to_string()),
1551 };
1552 assert!(payload.wire_validate().is_ok());
1553 }
1554
1555 #[test]
1556 fn set_update_freeze_reason_too_long() {
1557 let payload = SetUpdateFreezePayload {
1558 enabled: true,
1559 reason: Some("x".repeat(MAX_MEDIUM_STRING_LEN + 1)),
1560 };
1561 assert!(payload.wire_validate().is_err());
1562 }
1563
1564 #[test]
1565 fn release_asset_validates() {
1566 let asset = ReleaseAsset {
1567 name: "app.tar.gz".to_string(),
1568 download_url: "https://example.com/app".to_string(),
1569 size: None,
1570 content_type: None,
1571 sha256_digest: Some("a".repeat(64)),
1572 };
1573 assert!(asset.wire_validate().is_ok());
1574 }
1575
1576 #[test]
1577 fn release_asset_invalid_digest_wrong_length() {
1578 let asset = ReleaseAsset {
1579 name: "app.tar.gz".to_string(),
1580 download_url: "https://example.com/app".to_string(),
1581 size: None,
1582 content_type: None,
1583 sha256_digest: Some("abc".to_string()),
1584 };
1585 let err = asset.wire_validate().unwrap_err();
1586 assert_eq!(err.field, "asset.sha256_digest");
1587 }
1588
1589 #[test]
1590 fn release_asset_invalid_digest_non_hex() {
1591 let asset = ReleaseAsset {
1592 name: "app.tar.gz".to_string(),
1593 download_url: "https://example.com/app".to_string(),
1594 size: None,
1595 content_type: None,
1596 sha256_digest: Some("z".repeat(64)),
1597 };
1598 let err = asset.wire_validate().unwrap_err();
1599 assert_eq!(err.field, "asset.sha256_digest");
1600 }
1601
1602 #[test]
1603 fn release_info_validates() {
1604 let info = ReleaseInfo {
1605 tag: "v1.0.0".to_string(),
1606 release_url: "https://example.com/release".to_string(),
1607 assets: vec![],
1608 attestation_status: None,
1609 require_attestation: false,
1610 };
1611 assert!(info.wire_validate().is_ok());
1612 }
1613
1614 #[test]
1615 fn release_info_too_many_assets() {
1616 let assets: Vec<ReleaseAsset> = (0..MAX_RELEASE_ASSETS + 1)
1617 .map(|i| ReleaseAsset {
1618 name: format!("asset-{i}"),
1619 download_url: format!("https://example.com/{i}"),
1620 size: None,
1621 content_type: None,
1622 sha256_digest: None,
1623 })
1624 .collect();
1625 let info = ReleaseInfo {
1626 tag: "v1.0.0".to_string(),
1627 release_url: "https://example.com".to_string(),
1628 assets,
1629 attestation_status: None,
1630 require_attestation: false,
1631 };
1632 let err = info.wire_validate().unwrap_err();
1633 assert_eq!(err.field, "release_info.assets");
1634 }
1635
1636 #[test]
1637 fn execute_update_validates() {
1638 let payload = ExecuteUpdatePayload {
1639 host_machine_id: "test".to_string(),
1640 update_history_id: uuid::Uuid::nil(),
1641 software_item_id: uuid::Uuid::nil(),
1642 software_item_name: "test".to_string(),
1643 to_version: "1.0".to_string(),
1644 detect_version_plugin: None,
1645 execute_update_plugin: PluginAssignment {
1646 plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
1647 package_identifier: "test".to_string(),
1648 config: serde_json::json!({}),
1649 },
1650 pre_update_hook_plugins: vec![],
1651 post_update_hook_plugins: vec![],
1652 release_info: Some(ReleaseInfo {
1653 tag: "v1.0".to_string(),
1654 release_url: "https://example.com".to_string(),
1655 assets: vec![],
1656 attestation_status: None,
1657 require_attestation: false,
1658 }),
1659 timeout: std::time::Duration::from_secs(60),
1660 interactive: false,
1661 };
1662 assert!(payload.wire_validate().is_ok());
1663 }
1664
1665 #[test]
1666 fn execute_update_too_many_hook_plugins() {
1667 let plugins: Vec<PluginAssignment> = (0..MAX_UPDATE_HOOKS + 1)
1668 .map(|_| PluginAssignment {
1669 plugin_type: plugin_ids::HOOK_SHELL.clone(),
1670 package_identifier: String::new(),
1671 config: serde_json::json!({}),
1672 })
1673 .collect();
1674 let payload = ExecuteUpdatePayload {
1675 host_machine_id: "test".to_string(),
1676 update_history_id: uuid::Uuid::nil(),
1677 software_item_id: uuid::Uuid::nil(),
1678 software_item_name: "test".to_string(),
1679 to_version: "1.0".to_string(),
1680 detect_version_plugin: None,
1681 execute_update_plugin: PluginAssignment {
1682 plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
1683 package_identifier: "test".to_string(),
1684 config: serde_json::json!({}),
1685 },
1686 pre_update_hook_plugins: plugins,
1687 post_update_hook_plugins: vec![],
1688 release_info: None,
1689 timeout: std::time::Duration::from_secs(60),
1690 interactive: false,
1691 };
1692 let err = payload.wire_validate().unwrap_err();
1693 assert_eq!(err.field, "pre_update_hook_plugins");
1694 }
1695
1696 #[test]
1697 fn discovery_results_validates() {
1698 let payload = DiscoveryResultsPayload {
1699 host_machine_id: "test".to_string(),
1700 results: vec![],
1701 };
1702 assert!(payload.wire_validate().is_ok());
1703 }
1704
1705 #[test]
1706 fn unknown_service_message_passes() {
1707 let msg = ServiceMessage::Unknown;
1708 assert!(msg.wire_validate().is_ok());
1709 }
1710
1711 #[test]
1712 fn unknown_controller_message_passes() {
1713 let msg = ControllerMessage::Unknown;
1714 assert!(msg.wire_validate().is_ok());
1715 }
1716
1717 #[test]
1718 fn batch_update_result_validates() {
1719 let payload = BatchUpdateResultPayload {
1720 batch_id: uuid::Uuid::nil(),
1721 results: vec![],
1722 };
1723 assert!(payload.wire_validate().is_ok());
1724 }
1725
1726 #[test]
1727 fn batch_update_result_too_many() {
1728 let results: Vec<BatchUpdateItemResult> = (0..MAX_BATCH_UPDATE_RESULTS + 1)
1729 .map(|_| BatchUpdateItemResult {
1730 host_software_item_id: uuid::Uuid::nil(),
1731 update_history_id: uuid::Uuid::nil(),
1732 status: UpdateFinalStatus::Completed,
1733 output: String::new(),
1734 installed_version: None,
1735 error: None,
1736 })
1737 .collect();
1738 let payload = BatchUpdateResultPayload {
1739 batch_id: uuid::Uuid::nil(),
1740 results,
1741 };
1742 let err = payload.wire_validate().unwrap_err();
1743 assert_eq!(err.field, "results");
1744 }
1745
1746 fn test_surface_registration() -> surfaces::SurfaceRegistration {
1749 surfaces::SurfaceRegistration {
1750 provider: surfaces::ProviderIdentity {
1751 provider_id: "uptrakit-agent-ssh".to_string(),
1752 provider_kind: surfaces::ProviderKind::Service,
1753 provider_namespace: "uptrakit.agent.ssh".to_string(),
1754 },
1755 framework_generation: surfaces::FrameworkGeneration::new(1, 0),
1756 capabilities: surfaces::CapabilitySet::default(),
1757 effective_tenant_binding: surfaces::EffectiveTenantBinding {
1758 scope: surfaces::Scope::Tenant,
1759 tenant_id: Some(uuid::Uuid::nil().to_string()),
1760 },
1761 surfaces: vec![surfaces::RegisteredSurface {
1762 descriptor: surfaces::SurfaceDescriptor::builder()
1763 .surface_id(surfaces::SurfaceId::new("ssh.guest.panel").unwrap())
1764 .label("SSH Guests")
1765 .priority(100)
1766 .slot(surfaces::SLOT_SETTINGS_TABS)
1767 .scope(surfaces::Scope::Tenant)
1768 .targeting(surfaces::Targeting::Universal)
1769 .provider_kind(surfaces::ProviderKind::Service)
1770 .required_capabilities(surfaces::CapabilitySet::default())
1771 .root_node(surfaces::SurfaceNode::Section {
1772 title: Some("Guests".to_string()),
1773 children: vec![surfaces::SurfaceNode::TextBlock {
1774 text: "Guests view".to_string(),
1775 }],
1776 })
1777 .build(),
1778 interactions: vec![surfaces::InteractionDescriptor {
1779 interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
1780 kind: surfaces::InteractionKind::MutationAction,
1781 label: "Refresh".to_string(),
1782 required_permission: None,
1783 input_schema: None,
1784 result_schema: None,
1785 sensitive_fields: vec![],
1786 timeout_seconds: None,
1787 confirmation: None,
1788 transport: surfaces::InteractionTransport::ProviderProxied,
1789 workflow_steps: vec![],
1790 form_ui: None,
1791 }],
1792 data_sources: vec![surfaces::DataSourceDescriptor {
1793 data_source_id: surfaces::DataSourceId::new("guest.rows").unwrap(),
1794 kind: surfaces::DataSourceKind::Static {
1795 data: serde_json::json!({"rows": []}),
1796 },
1797 result_schema: surfaces::SchemaContract::Object,
1798 pagination: None,
1799 sorting: None,
1800 filtering: None,
1801 refresh_policy: surfaces::RefreshPolicy::Manual,
1802 empty_state: None,
1803 }],
1804 }],
1805 encryption_metadata: None,
1806 }
1807 }
1808
1809 fn nested_json_array(depth: usize) -> serde_json::Value {
1810 let mut value = serde_json::json!(0);
1811 for _ in 0..depth {
1812 value = serde_json::json!([value]);
1813 }
1814 value
1815 }
1816
1817 #[test]
1818 fn surface_registration_rejects_oversized_nested_root_node_text() {
1819 let mut payload = test_surface_registration();
1820 payload.surfaces[0].descriptor.root_node = surfaces::SurfaceNode::Section {
1821 title: None,
1822 children: vec![surfaces::SurfaceNode::Tabs {
1823 tabs: vec![surfaces::SurfaceTab {
1824 id: surfaces::SurfaceTabId::new("guests").unwrap(),
1825 label: "Guests".to_string(),
1826 root: surfaces::SurfaceNode::TextBlock {
1827 text: "x".repeat(MAX_MEDIUM_STRING_LEN + 1),
1828 },
1829 }],
1830 }],
1831 };
1832
1833 let err = payload.wire_validate().unwrap_err();
1834 assert_eq!(err.field, "surfaces[].descriptor.root_node.text");
1835 }
1836
1837 #[test]
1838 fn surface_registration_rejects_invalid_interaction_confirmation_text() {
1839 let mut payload = test_surface_registration();
1840 payload.surfaces[0].interactions[0] = surfaces::InteractionDescriptor {
1841 interaction_id: surfaces::InteractionId::new("danger.refresh").unwrap(),
1842 kind: surfaces::InteractionKind::ConfirmableAction,
1843 label: "Danger Refresh".to_string(),
1844 required_permission: None,
1845 input_schema: None,
1846 result_schema: None,
1847 sensitive_fields: vec![],
1848 timeout_seconds: None,
1849 confirmation: Some(surfaces::InteractionConfirmation {
1850 title: "Confirm".to_string(),
1851 message: "x".repeat(MAX_MEDIUM_STRING_LEN + 1),
1852 confirm_label: None,
1853 cancel_label: None,
1854 severity: surfaces::ConfirmationSeverity::Warning,
1855 }),
1856 transport: surfaces::InteractionTransport::ProviderProxied,
1857 workflow_steps: vec![],
1858 form_ui: None,
1859 };
1860
1861 let err = payload.wire_validate().unwrap_err();
1862 assert_eq!(err.field, "surfaces[].interactions[].confirmation.message");
1863 }
1864
1865 #[test]
1866 fn surface_registration_rejects_invalid_data_source_metadata() {
1867 let mut payload = test_surface_registration();
1868 payload.surfaces[0].data_sources[0] = surfaces::DataSourceDescriptor {
1869 data_source_id: surfaces::DataSourceId::new("guest.query").unwrap(),
1870 kind: surfaces::DataSourceKind::ProviderQuery {
1871 operation_id: "x".repeat(MAX_SHORT_STRING_LEN + 1),
1872 },
1873 result_schema: surfaces::SchemaContract::Object,
1874 pagination: Some(surfaces::DataSourcePagination {
1875 default_page_size: 100,
1876 max_page_size: 10,
1877 }),
1878 sorting: None,
1879 filtering: None,
1880 refresh_policy: surfaces::RefreshPolicy::Interval { seconds: 0 },
1881 empty_state: None,
1882 };
1883
1884 let err = payload.wire_validate().unwrap_err();
1885 assert_eq!(
1886 err.field,
1887 "surfaces[].data_sources[].kind.provider_query.operation_id"
1888 );
1889 }
1890
1891 #[test]
1892 fn surface_registration_rejects_overdeep_static_data() {
1893 let mut payload = test_surface_registration();
1894 payload.surfaces[0].data_sources[0] = surfaces::DataSourceDescriptor {
1895 data_source_id: surfaces::DataSourceId::new("guest.deep").unwrap(),
1896 kind: surfaces::DataSourceKind::Static {
1897 data: nested_json_array(MAX_SURFACE_JSON_DEPTH + 1),
1898 },
1899 result_schema: surfaces::SchemaContract::Array,
1900 pagination: None,
1901 sorting: None,
1902 filtering: None,
1903 refresh_policy: surfaces::RefreshPolicy::Manual,
1904 empty_state: None,
1905 };
1906
1907 let err = payload.wire_validate().unwrap_err();
1908 assert_eq!(err.field, "surfaces[].data_sources[].kind.static.data");
1909 }
1910
1911 #[test]
1912 fn surface_action_request_rejects_invalid_tenant_uuid() {
1913 let payload = surfaces::SurfaceActionRequest {
1914 request_id: uuid::Uuid::new_v4(),
1915 tenant_id: "not-a-uuid".to_string(),
1916 surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
1917 interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
1918 idempotency_key: "idem-1".to_string(),
1919 target_provider_id: None,
1920 caller_origin: surfaces::CallerOrigin::Provider {
1921 provider_id: "uptrakit-agent-ssh".to_string(),
1922 },
1923 params: serde_json::Map::new(),
1924 encrypted_sensitive_params: None,
1925 };
1926
1927 let err = payload.wire_validate().unwrap_err();
1928 assert_eq!(err.field, "tenant_id");
1929 }
1930
1931 #[test]
1932 fn surface_action_request_rejects_overdeep_params_json() {
1933 let payload = surfaces::SurfaceActionRequest {
1934 request_id: uuid::Uuid::new_v4(),
1935 tenant_id: uuid::Uuid::nil().to_string(),
1936 surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
1937 interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
1938 idempotency_key: "idem-1".to_string(),
1939 target_provider_id: None,
1940 caller_origin: surfaces::CallerOrigin::Provider {
1941 provider_id: "uptrakit-agent-ssh".to_string(),
1942 },
1943 params: serde_json::json!({
1944 "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
1945 })
1946 .as_object()
1947 .unwrap()
1948 .clone(),
1949 encrypted_sensitive_params: None,
1950 };
1951
1952 let err = payload.wire_validate().unwrap_err();
1953 assert_eq!(err.field, "params");
1954 }
1955
1956 #[test]
1957 fn surface_action_request_rejects_over_node_count_params_json() {
1958 let payload = surfaces::SurfaceActionRequest {
1959 request_id: uuid::Uuid::new_v4(),
1960 tenant_id: uuid::Uuid::nil().to_string(),
1961 surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
1962 interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
1963 idempotency_key: "idem-1".to_string(),
1964 target_provider_id: None,
1965 caller_origin: surfaces::CallerOrigin::Provider {
1966 provider_id: "uptrakit-agent-ssh".to_string(),
1967 },
1968 params: serde_json::json!({
1969 "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
1970 })
1971 .as_object()
1972 .unwrap()
1973 .clone(),
1974 encrypted_sensitive_params: None,
1975 };
1976
1977 let err = payload.wire_validate().unwrap_err();
1978 assert_eq!(err.field, "params");
1979 }
1980
1981 #[test]
1982 fn surface_action_response_rejects_overdeep_result_json() {
1983 let payload = surfaces::SurfaceActionResponse {
1984 request_id: uuid::Uuid::new_v4(),
1985 success: true,
1986 result: Some(serde_json::json!({
1987 "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
1988 })),
1989 error: None,
1990 };
1991
1992 let err = payload.wire_validate().unwrap_err();
1993 assert_eq!(err.field, "result");
1994 }
1995
1996 #[test]
1997 fn surface_action_response_rejects_over_node_count_result_json() {
1998 let payload = surfaces::SurfaceActionResponse {
1999 request_id: uuid::Uuid::new_v4(),
2000 success: true,
2001 result: Some(serde_json::json!({
2002 "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
2003 })),
2004 error: None,
2005 };
2006
2007 let err = payload.wire_validate().unwrap_err();
2008 assert_eq!(err.field, "result");
2009 }
2010
2011 #[test]
2012 fn surface_action_error_rejects_overdeep_details_json() {
2013 let payload = surfaces::SurfaceActionError {
2014 code: surfaces::SurfaceActionErrorCode::InternalError,
2015 message: "bad".to_string(),
2016 details: Some(serde_json::json!({
2017 "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
2018 })),
2019 };
2020
2021 let err = payload.wire_validate().unwrap_err();
2022 assert_eq!(err.field, "error.details");
2023 }
2024
2025 #[test]
2026 fn surface_action_error_rejects_over_node_count_details_json() {
2027 let payload = surfaces::SurfaceActionError {
2028 code: surfaces::SurfaceActionErrorCode::InternalError,
2029 message: "bad".to_string(),
2030 details: Some(serde_json::json!({
2031 "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
2032 })),
2033 };
2034
2035 let err = payload.wire_validate().unwrap_err();
2036 assert_eq!(err.field, "error.details");
2037 }
2038
2039 #[test]
2040 fn report_plugin_config_validates() {
2041 let msg = ServiceMessage::ReportPluginConfig(ReportPluginConfigPayload {
2042 request_id: "req-1".to_string(),
2043 plugin_type: "infrastructure_proxmox".to_string(),
2044 name: "pve.local".to_string(),
2045 config: serde_json::json!({"api_url": "https://pve:8006"}),
2046 });
2047 assert!(msg.wire_validate().is_ok());
2048 }
2049
2050 #[test]
2051 fn report_plugin_config_response_validates() {
2052 let msg =
2053 ControllerMessage::ReportPluginConfigResponse(ReportPluginConfigResponsePayload {
2054 request_id: "req-1".to_string(),
2055 success: true,
2056 plugin_config_id: Some(uuid::Uuid::nil()),
2057 error: None,
2058 });
2059 assert!(msg.wire_validate().is_ok());
2060 }
2061
2062 #[test]
2063 fn report_plugin_config_rejects_oversized_config() {
2064 let msg = ServiceMessage::ReportPluginConfig(ReportPluginConfigPayload {
2065 request_id: "req-1".to_string(),
2066 plugin_type: "infrastructure_proxmox".to_string(),
2067 name: "pve.local".to_string(),
2068 config: serde_json::Value::String("x".repeat(MAX_PLUGIN_CONFIG_JSON_LEN + 1)),
2069 });
2070 assert!(msg.wire_validate().is_err());
2071 }
2072
2073 #[test]
2074 fn update_stdin_data_validates() {
2075 let msg = ControllerMessage::UpdateStdinData(UpdateStdinDataPayload {
2076 update_history_id: uuid::Uuid::nil(),
2077 data: "aGVsbG8=".to_string(),
2078 signal: None,
2079 });
2080 assert!(msg.wire_validate().is_ok());
2081 }
2082
2083 #[test]
2084 fn update_stdin_data_rejects_oversized_data() {
2085 let msg = ControllerMessage::UpdateStdinData(UpdateStdinDataPayload {
2086 update_history_id: uuid::Uuid::nil(),
2087 data: "x".repeat(MAX_STDIN_DATA_LEN + 1),
2088 signal: None,
2089 });
2090 assert!(msg.wire_validate().is_err());
2091 }
2092
2093 #[test]
2094 fn stdin_attention_validates() {
2095 let msg = ServiceMessage::StdinAttention(StdinAttentionPayload {
2096 update_history_id: uuid::Uuid::nil(),
2097 hint: Some("waiting for config file conflict resolution".to_string()),
2098 });
2099 assert!(msg.wire_validate().is_ok());
2100 }
2101
2102 #[test]
2103 fn stdin_attention_rejects_oversized_hint() {
2104 let msg = ServiceMessage::StdinAttention(StdinAttentionPayload {
2105 update_history_id: uuid::Uuid::nil(),
2106 hint: Some("x".repeat(MAX_MEDIUM_STRING_LEN + 1)),
2107 });
2108 assert!(msg.wire_validate().is_err());
2109 }
2110
2111 #[test]
2112 fn service_settings_report_page_limits_validate() {
2113 let msg = ControllerMessage::ServiceSettings(ServiceSettingsPayload {
2114 renewal_window_hours: 6,
2115 ca_bundle_hash: "hash".to_string(),
2116 capabilities: std::collections::BTreeSet::new(),
2117 report_page_limits: ReportPageLimits::default(),
2118 shutdown_timeout: None,
2119 ping_interval: std::time::Duration::from_secs(30),
2120 tenant_id: None,
2121 });
2122
2123 assert!(msg.wire_validate().is_ok());
2124 }
2125
2126 #[test]
2127 fn service_settings_reject_zero_report_page_limit() {
2128 let msg = ControllerMessage::ServiceSettings(ServiceSettingsPayload {
2129 renewal_window_hours: 6,
2130 ca_bundle_hash: "hash".to_string(),
2131 capabilities: std::collections::BTreeSet::new(),
2132 report_page_limits: ReportPageLimits {
2133 report_hosts: 0,
2134 ..ReportPageLimits::default()
2135 },
2136 shutdown_timeout: None,
2137 ping_interval: std::time::Duration::from_secs(30),
2138 tenant_id: None,
2139 });
2140
2141 let err = msg.wire_validate().unwrap_err();
2142 assert_eq!(err.field, "report_page_limits.report_hosts");
2143 }
2144}