Skip to main content

unifly_api/integration/types/
policy.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use uuid::Uuid;
6
7/// Source endpoint of a firewall policy.
8#[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/// Destination endpoint of a firewall policy.
18#[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        // The controller sends `start`/`stop` (per the OpenAPI schema
247        // `Number range port matching`); `startPort`/`endPort` is kept as
248        // the serialization shape the write path has been tested with.
249        #[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    /// Integration UUID. Legacy user policies migrated from pre-zone firewall
331    /// config can arrive without one.
332    #[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// ── NAT Policies ────────────────────────────────────────────────────
524
525#[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        // The wire shape per the OpenAPI `Number range port matching` schema
666        // and the controller behavior reported in issue #26.
667        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}