Skip to main content

firewall_objects/
builder.rs

1//! Convenience helpers for assembling firewall objects in "builder" style.
2//!
3//! This module targets interactive or prototyping flows where developers want to express
4//! object definitions with as little boilerplate as possible. Helper functions turn dotted
5//! notation such as `address("app1", "192.0.2.10")` or `service::tcp(443)` into strongly
6//! typed objects that can be inserted into an [`ObjectStore`](crate::objects::ObjectStore).
7//!
8//! ```
9//! use firewall_objects::builder::{address, service, service_group};
10//! use firewall_objects::objects::ObjectStore;
11//!
12//! let mut store = ObjectStore::new();
13//!
14//! for entry in [
15//!     address("server1", "192.168.50.10").unwrap(),
16//!     address("Public DMZ", "10.10.105.0/24").unwrap(),
17//! ] {
18//!     store.add(entry).unwrap();
19//! }
20//!
21//! let allowed_services = service_group("allowed services")
22//!     .unwrap()
23//!     .add(service::tcp(443))
24//!     .unwrap()
25//!     .add(service::udp(53))
26//!     .unwrap()
27//!     .build()
28//!     .unwrap();
29//!
30//! store.add(allowed_services).unwrap();
31//! assert!(store.network("server1").is_ok());
32//! assert!(store.service_group("allowed services").is_ok());
33//! ```
34
35use std::collections::BTreeSet;
36use std::str::FromStr;
37
38use crate::ip::network::{Network, NetworkObj, NetworkObjGroup};
39use crate::service::{ApplicationObj, ServiceObj, ServiceObjGroup, TransportService};
40
41/// Unified wrapper returned by the builders so [`ObjectStore::add`](crate::objects::ObjectStore::add)
42/// can accept any object or builder directly.
43#[derive(Debug, Clone)]
44pub enum BuilderEntry {
45    Network(NetworkObj),
46    NetworkGroup(NetworkObjGroup),
47    Service(ServiceObj),
48    ServiceGroup(ServiceObjGroup),
49    Application(ApplicationObj),
50}
51
52impl From<NetworkObj> for BuilderEntry {
53    fn from(obj: NetworkObj) -> Self {
54        BuilderEntry::Network(obj)
55    }
56}
57
58impl From<NetworkObjGroup> for BuilderEntry {
59    fn from(group: NetworkObjGroup) -> Self {
60        BuilderEntry::NetworkGroup(group)
61    }
62}
63
64impl From<ServiceObj> for BuilderEntry {
65    fn from(obj: ServiceObj) -> Self {
66        BuilderEntry::Service(obj)
67    }
68}
69
70impl From<ServiceObjGroup> for BuilderEntry {
71    fn from(group: ServiceObjGroup) -> Self {
72        BuilderEntry::ServiceGroup(group)
73    }
74}
75
76impl From<ApplicationObj> for BuilderEntry {
77    fn from(app: ApplicationObj) -> Self {
78        BuilderEntry::Application(app)
79    }
80}
81
82/// Helper that builds a `NetworkObj` from a human-friendly input. The `name` is optional—pass
83/// an empty string to default to the normalized value.
84pub fn address(name: &str, value: &str) -> Result<NetworkObj, String> {
85    let network = Network::new(value)?;
86    let trimmed = name.trim();
87    let final_name = if trimmed.is_empty() {
88        network.to_string()
89    } else {
90        trimmed.to_string()
91    };
92    Ok(NetworkObj::new(final_name, network))
93}
94
95/// Start a network group builder with the provided `name`.
96pub fn network_group(name: &str) -> Result<NetworkGroupBuilder, String> {
97    NetworkGroupBuilder::new(name)
98}
99
100/// Start a service group builder with the provided `name`.
101pub fn service_group(name: &str) -> Result<ServiceGroupBuilder, String> {
102    ServiceGroupBuilder::new(name)
103}
104
105/// Begin composing an application object.
106pub fn application(name: &str, category: &str) -> Result<ApplicationBuilder, String> {
107    ApplicationBuilder::new(name, category)
108}
109
110/// Builder helper for adding multiple network objects into the same group.
111#[derive(Debug, Clone)]
112pub struct NetworkGroupBuilder {
113    name: String,
114    members: BTreeSet<NetworkObj>,
115}
116
117impl NetworkGroupBuilder {
118    pub fn new(name: &str) -> Result<Self, String> {
119        let trimmed = name.trim();
120        if trimmed.is_empty() {
121            return Err("network group name cannot be empty".into());
122        }
123        Ok(Self {
124            name: trimmed.to_string(),
125            members: BTreeSet::new(),
126        })
127    }
128
129    /// Add a network object or builder entry (created via [`address`]).
130    pub fn add<N>(mut self, entry: N) -> Result<Self, String>
131    where
132        N: IntoNetworkObj,
133    {
134        let obj = entry.into_network_obj();
135        if self.members.iter().any(|existing| existing.name == obj.name) {
136            return Err(format!(
137                "network object '{}' already exists in group '{}'",
138                obj.name, self.name
139            ));
140        }
141        self.members.insert(obj);
142        Ok(self)
143    }
144
145    pub fn build(self) -> Result<NetworkObjGroup, String> {
146        NetworkObjGroup::new(&self.name, self.members)
147    }
148}
149
150/// Builder helper for constructing service groups through chained calls.
151#[derive(Debug, Clone)]
152pub struct ServiceGroupBuilder {
153    name: String,
154    members: BTreeSet<ServiceObj>,
155}
156
157impl ServiceGroupBuilder {
158    pub fn new(name: &str) -> Result<Self, String> {
159        let trimmed = name.trim();
160        if trimmed.is_empty() {
161            return Err("service group name cannot be empty".into());
162        }
163        Ok(Self {
164            name: trimmed.to_string(),
165            members: BTreeSet::new(),
166        })
167    }
168
169    /// Add a service object or builder entry (created via [`service::tcp`] or friends).
170    pub fn add<S>(mut self, entry: S) -> Result<Self, String>
171    where
172        S: IntoServiceObj,
173    {
174        let obj = entry.into_service_obj();
175        if self.members.iter().any(|existing| existing.name == obj.name) {
176            return Err(format!(
177                "service object '{}' already exists in group '{}'",
178                obj.name, self.name
179            ));
180        }
181        self.members.insert(obj);
182        Ok(self)
183    }
184
185    pub fn build(self) -> Result<ServiceObjGroup, String> {
186        ServiceObjGroup::new(&self.name, self.members)
187    }
188}
189
190/// Network helper trait implemented for both raw objects and builder entries.
191pub trait IntoNetworkObj {
192    fn into_network_obj(self) -> NetworkObj;
193}
194
195impl IntoNetworkObj for NetworkObj {
196    fn into_network_obj(self) -> NetworkObj {
197        self
198    }
199}
200
201/// Service helper trait implemented for both raw objects and builder entries.
202pub trait IntoServiceObj {
203    fn into_service_obj(self) -> ServiceObj;
204}
205
206impl IntoServiceObj for ServiceObj {
207    fn into_service_obj(self) -> ServiceObj {
208        self
209    }
210}
211
212/// Convenience namespace for transport builders: `service::tcp(443)` or `service::parse("udp/53")`.
213pub mod service {
214    use super::*;
215
216    /// Create a TCP service entry with an auto-generated name (e.g., `"tcp/443"`).
217    pub fn tcp(port: u16) -> ServiceObj {
218        ServiceObj::new(format!("tcp/{port}"), TransportService::tcp(port))
219    }
220
221    /// Create a UDP service entry with an auto-generated name.
222    pub fn udp(port: u16) -> ServiceObj {
223        ServiceObj::new(format!("udp/{port}"), TransportService::udp(port))
224    }
225
226    /// Create an ICMPv4 service entry by type/code.
227    pub fn icmpv4(ty: u8, code: Option<u8>) -> ServiceObj {
228        let svc = TransportService::icmp(crate::service::icmp::IcmpVersion::V4, ty, code);
229        ServiceObj::new(svc.to_string(), svc)
230    }
231
232    /// Create an ICMPv6 service entry by type/code.
233    pub fn icmpv6(ty: u8, code: Option<u8>) -> ServiceObj {
234        let svc = TransportService::icmp(crate::service::icmp::IcmpVersion::V6, ty, code);
235        ServiceObj::new(svc.to_string(), svc)
236    }
237
238    /// Create a transport definition from a string such as `"tcp/22"` or `"icmp/echo-request"`.
239    pub fn parse(definition: &str) -> Result<ServiceObj, String> {
240        let svc = TransportService::from_str(definition)?;
241        let name = definition.trim();
242        let final_name = if name.is_empty() {
243            svc.to_string()
244        } else {
245            name.to_string()
246        };
247        Ok(ServiceObj::new(final_name, svc))
248    }
249}
250
251/// Builder for application objects with minimal ceremony.
252#[derive(Debug, Clone)]
253pub struct ApplicationBuilder {
254    name: String,
255    category: String,
256    transports: Vec<TransportService>,
257    dns_suffixes: Vec<String>,
258    tls_sni_suffixes: Vec<String>,
259    http_hosts: Vec<String>,
260}
261
262impl ApplicationBuilder {
263    pub fn new(name: &str, category: &str) -> Result<Self, String> {
264        let name = name.trim();
265        let category = category.trim();
266
267        if name.is_empty() {
268            return Err("application name cannot be empty".into());
269        }
270        if category.is_empty() {
271            return Err("application category cannot be empty".into());
272        }
273
274        Ok(Self {
275            name: name.to_string(),
276            category: category.to_string(),
277            transports: Vec::new(),
278            dns_suffixes: Vec::new(),
279            tls_sni_suffixes: Vec::new(),
280            http_hosts: Vec::new(),
281        })
282    }
283
284    /// Append a transport definition by parsing a descriptor like `"tcp/443"` or `"udp/53"`.
285    pub fn transport(mut self, descriptor: &str) -> Result<Self, String> {
286        self.transports.push(TransportService::from_str(descriptor)?);
287        Ok(self)
288    }
289
290    /// Append a pre-built transport.
291    pub fn transport_value(mut self, svc: TransportService) -> Self {
292        self.transports.push(svc);
293        self
294    }
295
296    pub fn dns_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
297        self.dns_suffixes.push(suffix.into());
298        self
299    }
300
301    pub fn tls_sni_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
302        self.tls_sni_suffixes.push(suffix.into());
303        self
304    }
305
306    pub fn http_host<S: Into<String>>(mut self, host: S) -> Self {
307        self.http_hosts.push(host.into());
308        self
309    }
310
311    pub fn build(self) -> ApplicationObj {
312        ApplicationObj {
313            name: self.name,
314            category: self.category,
315            transports: self.transports,
316            dns_suffixes: self.dns_suffixes,
317            tls_sni_suffixes: self.tls_sni_suffixes,
318            http_hosts: self.http_hosts,
319        }
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn address_defaults_name_when_empty() {
329        let obj = address("", "192.0.2.10").unwrap();
330        assert_eq!(obj.name, "192.0.2.10");
331    }
332
333    #[test]
334    fn service_group_builder_rejects_duplicates() {
335        let service = service::tcp(443);
336        let err = service_group("web")
337            .unwrap()
338            .add(service.clone())
339            .unwrap()
340            .add(service)
341            .unwrap_err();
342        assert!(err.contains("web"));
343    }
344
345    #[test]
346    fn application_builder_collects_values() {
347        let app = application("github", "developer")
348            .unwrap()
349            .transport("tcp/443")
350            .unwrap()
351            .dns_suffix(".github.com")
352            .http_host("github.com")
353            .build();
354
355        assert_eq!(app.name, "github");
356        assert_eq!(app.category, "developer");
357        assert_eq!(app.transports.len(), 1);
358        assert_eq!(app.dns_suffixes, vec![".github.com"]);
359        assert_eq!(app.http_hosts, vec!["github.com"]);
360    }
361}