1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use uuid::Uuid;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct FirewallPolicySource {
11 #[serde(default)]
12 pub zone_id: Option<Uuid>,
13 #[serde(default)]
14 pub traffic_filter: Option<SourceTrafficFilter>,
15}
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct FirewallPolicyDestination {
21 #[serde(default)]
22 pub zone_id: Option<Uuid>,
23 #[serde(default)]
24 pub traffic_filter: Option<DestTrafficFilter>,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(tag = "type")]
29pub enum SourceTrafficFilter {
30 #[serde(rename = "NETWORK")]
31 Network {
32 #[serde(rename = "networkFilter")]
33 network_filter: NetworkFilter,
34 #[serde(
35 rename = "macAddressFilter",
36 default,
37 skip_serializing_if = "Option::is_none"
38 )]
39 mac_address_filter: Option<MacAddressFilter>,
40 #[serde(
41 rename = "portFilter",
42 default,
43 skip_serializing_if = "Option::is_none"
44 )]
45 port_filter: Option<PortFilter>,
46 },
47 #[serde(rename = "IP_ADDRESS")]
48 IpAddress {
49 #[serde(rename = "ipAddressFilter")]
50 ip_address_filter: IpAddressFilter,
51 #[serde(
52 rename = "macAddressFilter",
53 default,
54 skip_serializing_if = "Option::is_none"
55 )]
56 mac_address_filter: Option<MacAddressFilter>,
57 #[serde(
58 rename = "portFilter",
59 default,
60 skip_serializing_if = "Option::is_none"
61 )]
62 port_filter: Option<PortFilter>,
63 },
64 #[serde(rename = "MAC_ADDRESS")]
65 MacAddress {
66 #[serde(rename = "macAddressFilter")]
67 mac_address_filter: MacAddressFilter,
68 #[serde(
69 rename = "portFilter",
70 default,
71 skip_serializing_if = "Option::is_none"
72 )]
73 port_filter: Option<PortFilter>,
74 },
75 #[serde(rename = "PORT")]
76 Port {
77 #[serde(rename = "portFilter")]
78 port_filter: PortFilter,
79 },
80 #[serde(rename = "REGION")]
81 Region {
82 #[serde(rename = "regionFilter")]
83 region_filter: RegionFilter,
84 #[serde(
85 rename = "portFilter",
86 default,
87 skip_serializing_if = "Option::is_none"
88 )]
89 port_filter: Option<PortFilter>,
90 },
91 #[serde(other)]
92 Unknown,
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96#[serde(tag = "type")]
97pub enum DestTrafficFilter {
98 #[serde(rename = "NETWORK")]
99 Network {
100 #[serde(rename = "networkFilter")]
101 network_filter: NetworkFilter,
102 #[serde(
103 rename = "portFilter",
104 default,
105 skip_serializing_if = "Option::is_none"
106 )]
107 port_filter: Option<PortFilter>,
108 },
109 #[serde(rename = "IP_ADDRESS")]
110 IpAddress {
111 #[serde(rename = "ipAddressFilter")]
112 ip_address_filter: IpAddressFilter,
113 #[serde(
114 rename = "portFilter",
115 default,
116 skip_serializing_if = "Option::is_none"
117 )]
118 port_filter: Option<PortFilter>,
119 },
120 #[serde(rename = "PORT")]
121 Port {
122 #[serde(rename = "portFilter")]
123 port_filter: PortFilter,
124 },
125 #[serde(rename = "REGION")]
126 Region {
127 #[serde(rename = "regionFilter")]
128 region_filter: RegionFilter,
129 #[serde(
130 rename = "portFilter",
131 default,
132 skip_serializing_if = "Option::is_none"
133 )]
134 port_filter: Option<PortFilter>,
135 },
136 #[serde(rename = "APPLICATION")]
137 Application {
138 #[serde(rename = "applicationFilter")]
139 application_filter: ApplicationFilter,
140 #[serde(
141 rename = "portFilter",
142 default,
143 skip_serializing_if = "Option::is_none"
144 )]
145 port_filter: Option<PortFilter>,
146 },
147 #[serde(rename = "APPLICATION_CATEGORY")]
148 ApplicationCategory {
149 #[serde(rename = "applicationCategoryFilter")]
150 application_category_filter: ApplicationCategoryFilter,
151 #[serde(
152 rename = "portFilter",
153 default,
154 skip_serializing_if = "Option::is_none"
155 )]
156 port_filter: Option<PortFilter>,
157 },
158 #[serde(rename = "DOMAIN")]
159 Domain {
160 #[serde(rename = "domainFilter")]
161 domain_filter: DomainFilter,
162 #[serde(
163 rename = "portFilter",
164 default,
165 skip_serializing_if = "Option::is_none"
166 )]
167 port_filter: Option<PortFilter>,
168 },
169 #[serde(other)]
170 Unknown,
171}
172
173#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct NetworkFilter {
176 pub network_ids: Vec<Uuid>,
177 #[serde(default)]
178 pub match_opposite: bool,
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182#[serde(tag = "type")]
183pub enum IpAddressFilter {
184 #[serde(rename = "IP_ADDRESSES", alias = "SPECIFIC")]
185 Specific {
186 #[serde(default)]
187 items: Vec<IpAddressItem>,
188 #[serde(default, rename = "matchOpposite")]
189 match_opposite: bool,
190 },
191 #[serde(rename = "TRAFFIC_MATCHING_LIST")]
192 TrafficMatchingList {
193 #[serde(rename = "trafficMatchingListId")]
194 traffic_matching_list_id: Uuid,
195 #[serde(default, rename = "matchOpposite")]
196 match_opposite: bool,
197 },
198 #[serde(other)]
199 Unknown,
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203#[serde(tag = "type")]
204pub enum IpAddressItem {
205 #[serde(rename = "IP_ADDRESS")]
206 Address { value: String },
207 #[serde(rename = "RANGE")]
208 Range { start: String, stop: String },
209 #[serde(rename = "SUBNET")]
210 Subnet { value: String },
211 #[serde(other)]
212 Unknown,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[serde(tag = "type")]
217pub enum PortFilter {
218 #[serde(rename = "PORTS", alias = "VALUE")]
219 Ports {
220 #[serde(default)]
221 items: Vec<PortItem>,
222 #[serde(default, rename = "matchOpposite")]
223 match_opposite: bool,
224 },
225 #[serde(rename = "TRAFFIC_MATCHING_LIST")]
226 TrafficMatchingList {
227 #[serde(rename = "trafficMatchingListId")]
228 traffic_matching_list_id: Uuid,
229 #[serde(default, rename = "matchOpposite")]
230 match_opposite: bool,
231 },
232 #[serde(other)]
233 Unknown,
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
237#[serde(tag = "type")]
238pub enum PortItem {
239 #[serde(rename = "PORT_NUMBER")]
240 Number {
241 #[serde(deserialize_with = "deserialize_port_value")]
242 value: String,
243 },
244 #[serde(rename = "PORT_NUMBER_RANGE", alias = "PORT_RANGE")]
245 Range {
246 #[serde(
250 rename = "startPort",
251 alias = "start",
252 deserialize_with = "deserialize_port_value"
253 )]
254 start_port: String,
255 #[serde(
256 rename = "endPort",
257 alias = "stop",
258 deserialize_with = "deserialize_port_value"
259 )]
260 end_port: String,
261 },
262 #[serde(other)]
263 Unknown,
264}
265
266fn deserialize_port_value<'de, D>(deserializer: D) -> Result<String, D::Error>
267where
268 D: serde::Deserializer<'de>,
269{
270 struct PortValueVisitor;
271
272 impl serde::de::Visitor<'_> for PortValueVisitor {
273 type Value = String;
274
275 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
276 formatter.write_str("a port number as string or integer")
277 }
278
279 fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<String, E> {
280 Ok(value.to_string())
281 }
282
283 fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<String, E> {
284 Ok(value.to_string())
285 }
286
287 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<String, E> {
288 Ok(value.to_string())
289 }
290 }
291
292 deserializer.deserialize_any(PortValueVisitor)
293}
294
295#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct MacAddressFilter {
298 pub mac_addresses: Vec<String>,
299}
300
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct ApplicationFilter {
304 pub application_ids: Vec<i64>,
305}
306
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub struct ApplicationCategoryFilter {
310 pub application_category_ids: Vec<i64>,
311}
312
313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314pub struct RegionFilter {
315 pub regions: Vec<String>,
316}
317
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
319#[serde(tag = "type")]
320pub enum DomainFilter {
321 #[serde(rename = "SPECIFIC")]
322 Specific { domains: Vec<String> },
323 #[serde(other)]
324 Unknown,
325}
326
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328#[serde(rename_all = "camelCase")]
329pub struct FirewallPolicyResponse {
330 #[serde(default)]
333 pub id: Option<Uuid>,
334 pub name: String,
335 #[serde(default)]
336 pub description: Option<String>,
337 pub enabled: bool,
338 pub action: Value,
339 pub ip_protocol_scope: Option<Value>,
340 #[serde(default)]
341 pub logging_enabled: bool,
342 pub metadata: Option<Value>,
343 #[serde(default)]
344 pub source: Option<FirewallPolicySource>,
345 #[serde(default)]
346 pub destination: Option<FirewallPolicyDestination>,
347 #[serde(flatten)]
348 pub extra: HashMap<String, Value>,
349}
350
351#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
352#[serde(rename_all = "camelCase")]
353pub struct FirewallPolicyCreateUpdate {
354 pub name: String,
355 pub description: Option<String>,
356 pub enabled: bool,
357 pub action: Value,
358 pub source: Value,
359 pub destination: Value,
360 pub ip_protocol_scope: Value,
361 pub logging_enabled: bool,
362 pub ipsec_filter: Option<String>,
363 pub schedule: Option<Value>,
364 pub connection_state_filter: Option<Vec<String>>,
365}
366
367#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct FirewallPolicyPatch {
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub enabled: Option<bool>,
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub logging_enabled: Option<bool>,
374}
375
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct FirewallPolicyOrderingEnvelope {
379 pub ordered_firewall_policy_ids: FirewallPolicyOrdering,
380}
381
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase")]
384pub struct FirewallPolicyOrdering {
385 pub before_system_defined: Vec<Uuid>,
386 pub after_system_defined: Vec<Uuid>,
387}
388
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390#[serde(rename_all = "camelCase")]
391pub struct FirewallZoneResponse {
392 pub id: Uuid,
393 pub name: String,
394 pub network_ids: Vec<Uuid>,
395 pub metadata: Value,
396}
397
398#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct FirewallZoneCreateUpdate {
401 pub name: String,
402 pub network_ids: Vec<Uuid>,
403}
404
405#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
406#[serde(rename_all = "camelCase")]
407pub struct AclRuleResponse {
408 pub id: Uuid,
409 pub name: String,
410 #[serde(rename = "type")]
411 pub rule_type: String,
412 pub action: String,
413 pub enabled: bool,
414 pub index: i32,
415 pub description: Option<String>,
416 pub source_filter: Option<Value>,
417 pub destination_filter: Option<Value>,
418 pub enforcing_device_filter: Option<Value>,
419 pub metadata: Value,
420}
421
422#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
423#[serde(rename_all = "camelCase")]
424pub struct AclRuleCreateUpdate {
425 pub name: String,
426 #[serde(rename = "type")]
427 pub rule_type: String,
428 pub action: String,
429 pub enabled: bool,
430 pub description: Option<String>,
431 pub source_filter: Option<Value>,
432 pub destination_filter: Option<Value>,
433 pub enforcing_device_filter: Option<Value>,
434}
435
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437#[serde(rename_all = "camelCase")]
438pub struct AclRuleOrdering {
439 pub ordered_acl_rule_ids: Vec<Uuid>,
440}
441
442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
443#[serde(rename_all = "camelCase")]
444pub struct DnsPolicyResponse {
445 pub id: Uuid,
446 #[serde(rename = "type")]
447 pub policy_type: String,
448 pub enabled: bool,
449 pub domain: Option<String>,
450 pub metadata: Value,
451 #[serde(flatten)]
452 pub extra: HashMap<String, Value>,
453}
454
455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
456#[serde(rename_all = "camelCase")]
457pub struct DnsPolicyCreateUpdate {
458 #[serde(rename = "type")]
459 pub policy_type: String,
460 pub enabled: bool,
461 #[serde(flatten)]
462 pub fields: serde_json::Map<String, Value>,
463}
464
465#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct TrafficMatchingListResponse {
468 pub id: Uuid,
469 pub name: String,
470 #[serde(rename = "type")]
471 pub list_type: String,
472 #[serde(flatten)]
473 pub extra: HashMap<String, Value>,
474}
475
476#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
477#[serde(rename_all = "camelCase")]
478pub struct TrafficMatchingListCreateUpdate {
479 pub name: String,
480 #[serde(rename = "type")]
481 pub list_type: String,
482 #[serde(flatten)]
483 pub fields: serde_json::Map<String, Value>,
484}
485
486#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
487#[serde(rename_all = "camelCase")]
488pub struct VoucherResponse {
489 pub id: Uuid,
490 pub code: String,
491 pub name: String,
492 pub created_at: String,
493 pub activated_at: Option<String>,
494 pub expires_at: Option<String>,
495 pub expired: bool,
496 pub time_limit_minutes: i64,
497 pub authorized_guest_count: i64,
498 pub authorized_guest_limit: Option<i64>,
499 pub data_usage_limit_m_bytes: Option<i64>,
500 pub rx_rate_limit_kbps: Option<i64>,
501 pub tx_rate_limit_kbps: Option<i64>,
502}
503
504#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
505#[serde(rename_all = "camelCase")]
506pub struct VoucherCreateRequest {
507 pub name: String,
508 pub count: Option<i32>,
509 pub time_limit_minutes: i64,
510 pub authorized_guest_limit: Option<i64>,
511 pub data_usage_limit_m_bytes: Option<i64>,
512 pub rx_rate_limit_kbps: Option<i64>,
513 pub tx_rate_limit_kbps: Option<i64>,
514}
515
516#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
517#[serde(rename_all = "camelCase")]
518pub struct VoucherDeletionResults {
519 #[serde(flatten)]
520 pub fields: HashMap<String, Value>,
521}
522
523#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
526#[serde(rename_all = "camelCase")]
527pub struct NatPolicyResponse {
528 pub id: Uuid,
529 pub name: String,
530 #[serde(default)]
531 pub description: Option<String>,
532 pub enabled: bool,
533 #[serde(rename = "type")]
534 pub nat_type: String,
535 #[serde(default)]
536 pub interface_id: Option<Uuid>,
537 #[serde(default)]
538 pub protocol: Option<String>,
539 #[serde(default)]
540 pub source: Option<Value>,
541 #[serde(default)]
542 pub destination: Option<Value>,
543 #[serde(default)]
544 pub translated_address: Option<String>,
545 #[serde(default)]
546 pub translated_port: Option<String>,
547 pub metadata: Option<Value>,
548 #[serde(flatten)]
549 pub extra: HashMap<String, Value>,
550}
551
552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
553#[serde(rename_all = "camelCase")]
554pub struct NatPolicyCreateUpdate {
555 pub name: String,
556 #[serde(skip_serializing_if = "Option::is_none")]
557 pub description: Option<String>,
558 pub enabled: bool,
559 #[serde(rename = "type")]
560 pub nat_type: String,
561 #[serde(skip_serializing_if = "Option::is_none")]
562 pub interface_id: Option<Uuid>,
563 #[serde(skip_serializing_if = "Option::is_none")]
564 pub protocol: Option<String>,
565 #[serde(skip_serializing_if = "Option::is_none")]
566 pub source: Option<Value>,
567 #[serde(skip_serializing_if = "Option::is_none")]
568 pub destination: Option<Value>,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 pub translated_address: Option<String>,
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub translated_port: Option<String>,
573}
574
575#[cfg(test)]
576#[allow(clippy::unwrap_used)]
577mod tests {
578 use super::{IpAddressFilter, PortFilter, PortItem};
579
580 #[test]
581 fn port_filter_accepts_value_alias_and_numeric_ports() {
582 let filter: PortFilter = serde_json::from_value(serde_json::json!({
583 "type": "VALUE",
584 "items": [
585 { "type": "PORT_NUMBER", "value": 443 },
586 { "type": "PORT_RANGE", "startPort": 8000, "endPort": "9000" }
587 ],
588 "matchOpposite": true
589 }))
590 .unwrap();
591
592 match filter {
593 PortFilter::Ports {
594 items,
595 match_opposite,
596 } => {
597 assert!(match_opposite);
598 assert_eq!(
599 items,
600 vec![
601 PortItem::Number {
602 value: "443".into()
603 },
604 PortItem::Range {
605 start_port: "8000".into(),
606 end_port: "9000".into()
607 },
608 ]
609 );
610 }
611 other => panic!("unexpected filter: {other:?}"),
612 }
613 }
614
615 #[test]
616 fn ip_address_filter_accepts_specific_alias() {
617 let filter: IpAddressFilter = serde_json::from_value(serde_json::json!({
618 "type": "SPECIFIC",
619 "items": [{ "type": "IP_ADDRESS", "value": "192.168.1.10" }],
620 "matchOpposite": false
621 }))
622 .unwrap();
623
624 assert!(matches!(filter, IpAddressFilter::Specific { .. }));
625 }
626
627 #[test]
628 fn port_item_range_serializes_as_port_number_range() {
629 let item = PortItem::Range {
630 start_port: "49152".into(),
631 end_port: "65535".into(),
632 };
633 let json = serde_json::to_value(&item).unwrap();
634 assert_eq!(
635 json.get("type").and_then(serde_json::Value::as_str),
636 Some("PORT_NUMBER_RANGE"),
637 "Range must serialize as PORT_NUMBER_RANGE, not PORT_RANGE"
638 );
639 assert_eq!(
640 json.get("startPort").and_then(serde_json::Value::as_str),
641 Some("49152")
642 );
643 assert_eq!(
644 json.get("endPort").and_then(serde_json::Value::as_str),
645 Some("65535")
646 );
647 }
648
649 #[test]
650 fn port_item_range_deserializes_from_port_range_alias() {
651 let item: PortItem = serde_json::from_value(serde_json::json!({
652 "type": "PORT_RANGE",
653 "startPort": "8000",
654 "endPort": "9000"
655 }))
656 .unwrap();
657 assert!(
658 matches!(item, PortItem::Range { .. }),
659 "PORT_RANGE alias must still deserialize"
660 );
661 }
662
663 #[test]
664 fn port_item_range_deserializes_from_spec_start_stop_fields() {
665 let item: PortItem = serde_json::from_value(serde_json::json!({
668 "type": "PORT_NUMBER_RANGE",
669 "start": 8000,
670 "stop": 9000
671 }))
672 .unwrap();
673 assert_eq!(
674 item,
675 PortItem::Range {
676 start_port: "8000".into(),
677 end_port: "9000".into()
678 }
679 );
680 }
681
682 #[test]
683 fn ip_address_item_tolerates_unknown_type() {
684 let filter: IpAddressFilter = serde_json::from_value(serde_json::json!({
685 "type": "SPECIFIC",
686 "items": [{ "type": "GEOIP_FANCY_FUTURE", "value": "whatever" }],
687 "matchOpposite": false
688 }))
689 .unwrap();
690
691 match filter {
692 IpAddressFilter::Specific { items, .. } => {
693 assert_eq!(items, vec![super::IpAddressItem::Unknown]);
694 }
695 other => panic!("unexpected filter: {other:?}"),
696 }
697 }
698}