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