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