Skip to main content

unifly_api/model/
firewall.rs

1// ── Firewall domain types ──
2
3use serde::{Deserialize, Serialize};
4
5use super::common::{DataSource, EntityOrigin};
6use super::entity_id::EntityId;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum FirewallAction {
10    Allow,
11    Block,
12    Reject,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum IpVersion {
17    Ipv4,
18    Ipv6,
19    Both,
20}
21
22/// Firewall Zone -- container for networks, policies operate between zones.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct FirewallZone {
25    pub id: EntityId,
26    pub name: String,
27    pub network_ids: Vec<EntityId>,
28    pub origin: Option<EntityOrigin>,
29
30    #[serde(skip)]
31    #[allow(dead_code)]
32    pub(crate) source: DataSource,
33}
34
35// ── Traffic filter types ─────────────────────────────────────────
36
37/// Source endpoint with zone and optional traffic filter.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct PolicyEndpoint {
40    pub zone_id: Option<EntityId>,
41    pub filter: Option<TrafficFilter>,
42}
43
44/// Traffic filter applied to a source or destination.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(tag = "kind", rename_all = "snake_case")]
47pub enum TrafficFilter {
48    Network {
49        network_ids: Vec<EntityId>,
50        match_opposite: bool,
51        #[serde(default, skip_serializing_if = "Vec::is_empty")]
52        mac_addresses: Vec<String>,
53        #[serde(default, skip_serializing_if = "Option::is_none")]
54        ports: Option<PortSpec>,
55    },
56    IpAddress {
57        addresses: Vec<IpSpec>,
58        match_opposite: bool,
59        #[serde(default, skip_serializing_if = "Vec::is_empty")]
60        mac_addresses: Vec<String>,
61        #[serde(default, skip_serializing_if = "Option::is_none")]
62        ports: Option<PortSpec>,
63    },
64    MacAddress {
65        mac_addresses: Vec<String>,
66        #[serde(default, skip_serializing_if = "Option::is_none")]
67        ports: Option<PortSpec>,
68    },
69    Port {
70        ports: PortSpec,
71    },
72    Region {
73        regions: Vec<String>,
74        #[serde(default, skip_serializing_if = "Option::is_none")]
75        ports: Option<PortSpec>,
76    },
77    Application {
78        application_ids: Vec<i64>,
79        #[serde(default, skip_serializing_if = "Option::is_none")]
80        ports: Option<PortSpec>,
81    },
82    ApplicationCategory {
83        category_ids: Vec<i64>,
84        #[serde(default, skip_serializing_if = "Option::is_none")]
85        ports: Option<PortSpec>,
86    },
87    Domain {
88        domains: Vec<String>,
89        #[serde(default, skip_serializing_if = "Option::is_none")]
90        ports: Option<PortSpec>,
91    },
92    /// Catch-all for filter types not yet modeled.
93    Other {
94        raw_type: String,
95    },
96}
97
98/// Port specification.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(tag = "kind", rename_all = "snake_case")]
101pub enum PortSpec {
102    Values {
103        items: Vec<String>,
104        match_opposite: bool,
105    },
106    MatchingList {
107        list_id: EntityId,
108        match_opposite: bool,
109    },
110}
111
112/// IP address specification.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(tag = "kind", rename_all = "snake_case")]
115pub enum IpSpec {
116    Address { value: String },
117    Range { start: String, stop: String },
118    Subnet { value: String },
119    MatchingList { list_id: EntityId },
120}
121
122impl TrafficFilter {
123    /// Human-readable summary for table display.
124    pub fn summary(&self) -> String {
125        match self {
126            Self::Network {
127                network_ids,
128                match_opposite,
129                ..
130            } => {
131                let prefix = if *match_opposite { "NOT " } else { "" };
132                format!("{prefix}net({} networks)", network_ids.len())
133            }
134            Self::IpAddress {
135                addresses,
136                match_opposite,
137                ..
138            } => {
139                let prefix = if *match_opposite { "NOT " } else { "" };
140                let items: Vec<String> = addresses
141                    .iter()
142                    .map(|a| match a {
143                        IpSpec::Address { value } | IpSpec::Subnet { value } => value.clone(),
144                        IpSpec::Range { start, stop } => format!("{start}-{stop}"),
145                        IpSpec::MatchingList { list_id } => format!("list:{list_id}"),
146                    })
147                    .collect();
148                let display = if items.len() <= 2 {
149                    items.join(", ")
150                } else {
151                    format!("{}, {} +{} more", items[0], items[1], items.len() - 2)
152                };
153                format!("{prefix}ip({display})")
154            }
155            Self::MacAddress { mac_addresses, .. } => {
156                format!("mac({})", mac_addresses.len())
157            }
158            Self::Port { ports } => summarize_ports(ports),
159            Self::Region { regions, .. } => format!("region({})", regions.join(",")),
160            Self::Application {
161                application_ids, ..
162            } => {
163                format!("app({} apps)", application_ids.len())
164            }
165            Self::ApplicationCategory { category_ids, .. } => {
166                format!("cat({} categories)", category_ids.len())
167            }
168            Self::Domain { domains, .. } => {
169                if domains.len() <= 2 {
170                    format!("domain({})", domains.join(", "))
171                } else {
172                    format!("domain({} +{} more)", domains[0], domains.len() - 1)
173                }
174            }
175            Self::Other { raw_type } => format!("({raw_type})"),
176        }
177    }
178}
179
180fn summarize_ports(spec: &PortSpec) -> String {
181    match spec {
182        PortSpec::Values {
183            items,
184            match_opposite,
185        } => {
186            let prefix = if *match_opposite { "NOT " } else { "" };
187            format!("{prefix}port({})", items.join(","))
188        }
189        PortSpec::MatchingList {
190            list_id,
191            match_opposite,
192        } => {
193            let prefix = if *match_opposite { "NOT " } else { "" };
194            format!("{prefix}port(list:{list_id})")
195        }
196    }
197}
198
199/// Firewall Policy -- a rule between two zones.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct FirewallPolicy {
202    pub id: EntityId,
203    pub name: String,
204    pub description: Option<String>,
205    pub enabled: bool,
206    pub index: Option<i32>,
207
208    pub action: FirewallAction,
209    pub ip_version: IpVersion,
210
211    // Structured source/destination with traffic filters
212    pub source: PolicyEndpoint,
213    pub destination: PolicyEndpoint,
214
215    // Human-readable summaries (computed from filters)
216    pub source_summary: Option<String>,
217    pub destination_summary: Option<String>,
218
219    // Protocol and schedule display fields
220    pub protocol_summary: Option<String>,
221    pub schedule: Option<String>,
222    pub ipsec_mode: Option<String>,
223
224    pub connection_states: Vec<String>,
225    pub logging_enabled: bool,
226
227    pub origin: Option<EntityOrigin>,
228
229    #[serde(skip)]
230    #[allow(dead_code)]
231    pub(crate) data_source: DataSource,
232}
233
234/// ACL Rule action.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
236pub enum AclAction {
237    Allow,
238    Block,
239}
240
241/// ACL Rule type.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243pub enum AclRuleType {
244    Ipv4,
245    Mac,
246}
247
248/// ACL Rule.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct AclRule {
251    pub id: EntityId,
252    pub name: String,
253    pub enabled: bool,
254    pub rule_type: AclRuleType,
255    pub action: AclAction,
256    pub source_summary: Option<String>,
257    pub destination_summary: Option<String>,
258    pub origin: Option<EntityOrigin>,
259
260    #[serde(skip)]
261    #[allow(dead_code)]
262    pub(crate) source: DataSource,
263}