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//!     .with_service(service::tcp(443))
24//!     .unwrap()
25//!     .with_service(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    /// Include a network object or builder entry (created via [`address`]).
130    pub fn with_network<N>(mut self, entry: N) -> Result<Self, String>
131    where
132        N: IntoNetworkObj,
133    {
134        let obj = entry.into_network_obj();
135        if self
136            .members
137            .iter()
138            .any(|existing| existing.name == obj.name)
139        {
140            return Err(format!(
141                "network object '{}' already exists in group '{}'",
142                obj.name, self.name
143            ));
144        }
145        self.members.insert(obj);
146        Ok(self)
147    }
148
149    pub fn build(self) -> Result<NetworkObjGroup, String> {
150        NetworkObjGroup::new(&self.name, self.members)
151    }
152}
153
154/// Builder helper for constructing service groups through chained calls.
155#[derive(Debug, Clone)]
156pub struct ServiceGroupBuilder {
157    name: String,
158    members: BTreeSet<ServiceObj>,
159}
160
161impl ServiceGroupBuilder {
162    pub fn new(name: &str) -> Result<Self, String> {
163        let trimmed = name.trim();
164        if trimmed.is_empty() {
165            return Err("service group name cannot be empty".into());
166        }
167        Ok(Self {
168            name: trimmed.to_string(),
169            members: BTreeSet::new(),
170        })
171    }
172
173    /// Include a service object or builder entry (created via [`service::tcp`] or friends).
174    pub fn with_service<S>(mut self, entry: S) -> Result<Self, String>
175    where
176        S: IntoServiceObj,
177    {
178        let obj = entry.into_service_obj();
179        if self
180            .members
181            .iter()
182            .any(|existing| existing.name == obj.name)
183        {
184            return Err(format!(
185                "service object '{}' already exists in group '{}'",
186                obj.name, self.name
187            ));
188        }
189        self.members.insert(obj);
190        Ok(self)
191    }
192
193    pub fn build(self) -> Result<ServiceObjGroup, String> {
194        ServiceObjGroup::new(&self.name, self.members)
195    }
196}
197
198/// Network helper trait implemented for both raw objects and builder entries.
199pub trait IntoNetworkObj {
200    fn into_network_obj(self) -> NetworkObj;
201}
202
203impl IntoNetworkObj for NetworkObj {
204    fn into_network_obj(self) -> NetworkObj {
205        self
206    }
207}
208
209/// Service helper trait implemented for both raw objects and builder entries.
210pub trait IntoServiceObj {
211    fn into_service_obj(self) -> ServiceObj;
212}
213
214impl IntoServiceObj for ServiceObj {
215    fn into_service_obj(self) -> ServiceObj {
216        self
217    }
218}
219
220/// Convenience namespace for transport builders: `service::tcp(443)` or `service::parse("udp/53")`.
221pub mod service {
222    use super::*;
223
224    /// Create a TCP service entry with an auto-generated name (e.g., `"tcp/443"`).
225    pub fn tcp(port: u16) -> ServiceObj {
226        ServiceObj::new(format!("tcp/{port}"), TransportService::tcp(port))
227    }
228
229    /// Create a UDP service entry with an auto-generated name.
230    pub fn udp(port: u16) -> ServiceObj {
231        ServiceObj::new(format!("udp/{port}"), TransportService::udp(port))
232    }
233
234    /// Create an ICMPv4 service entry by type/code.
235    pub fn icmpv4(ty: u8, code: Option<u8>) -> ServiceObj {
236        let svc = TransportService::icmp(crate::service::icmp::IcmpVersion::V4, ty, code);
237        ServiceObj::new(svc.to_string(), svc)
238    }
239
240    /// Create an ICMPv6 service entry by type/code.
241    pub fn icmpv6(ty: u8, code: Option<u8>) -> ServiceObj {
242        let svc = TransportService::icmp(crate::service::icmp::IcmpVersion::V6, ty, code);
243        ServiceObj::new(svc.to_string(), svc)
244    }
245
246    /// Create a transport definition from a string such as `"tcp/22"` or `"icmp/echo-request"`.
247    pub fn parse(definition: &str) -> Result<ServiceObj, String> {
248        let svc = TransportService::from_str(definition)?;
249        let name = definition.trim();
250        let final_name = if name.is_empty() {
251            svc.to_string()
252        } else {
253            name.to_string()
254        };
255        Ok(ServiceObj::new(final_name, svc))
256    }
257}
258
259/// Builder for application objects with minimal ceremony.
260#[derive(Debug, Clone)]
261pub struct ApplicationBuilder {
262    name: String,
263    category: String,
264    transports: Vec<TransportService>,
265    dns_suffixes: Vec<String>,
266    tls_sni_suffixes: Vec<String>,
267    http_hosts: Vec<String>,
268}
269
270impl ApplicationBuilder {
271    pub fn new(name: &str, category: &str) -> Result<Self, String> {
272        let name = name.trim();
273        let category = category.trim();
274
275        if name.is_empty() {
276            return Err("application name cannot be empty".into());
277        }
278        if category.is_empty() {
279            return Err("application category cannot be empty".into());
280        }
281
282        Ok(Self {
283            name: name.to_string(),
284            category: category.to_string(),
285            transports: Vec::new(),
286            dns_suffixes: Vec::new(),
287            tls_sni_suffixes: Vec::new(),
288            http_hosts: Vec::new(),
289        })
290    }
291
292    /// Append a transport definition by parsing a descriptor like `"tcp/443"` or `"udp/53"`.
293    pub fn transport(mut self, descriptor: &str) -> Result<Self, String> {
294        self.transports
295            .push(TransportService::from_str(descriptor)?);
296        Ok(self)
297    }
298
299    /// Append a pre-built transport.
300    pub fn transport_value(mut self, svc: TransportService) -> Self {
301        self.transports.push(svc);
302        self
303    }
304
305    pub fn dns_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
306        self.dns_suffixes.push(suffix.into());
307        self
308    }
309
310    pub fn tls_sni_suffix<S: Into<String>>(mut self, suffix: S) -> Self {
311        self.tls_sni_suffixes.push(suffix.into());
312        self
313    }
314
315    pub fn http_host<S: Into<String>>(mut self, host: S) -> Self {
316        self.http_hosts.push(host.into());
317        self
318    }
319
320    pub fn build(self) -> ApplicationObj {
321        ApplicationObj {
322            name: self.name,
323            category: self.category,
324            transports: self.transports,
325            dns_suffixes: self.dns_suffixes,
326            tls_sni_suffixes: self.tls_sni_suffixes,
327            http_hosts: self.http_hosts,
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn address_defaults_name_when_empty() {
338        let obj = address("", "192.0.2.10").unwrap();
339        assert_eq!(obj.name, "192.0.2.10");
340    }
341
342    #[test]
343    fn service_group_builder_rejects_duplicates() {
344        let service = service::tcp(443);
345        let err = service_group("web")
346            .unwrap()
347            .with_service(service.clone())
348            .unwrap()
349            .with_service(service)
350            .unwrap_err();
351        assert!(err.contains("web"));
352    }
353
354    #[test]
355    fn application_builder_collects_values() {
356        let app = application("github", "developer")
357            .unwrap()
358            .transport("tcp/443")
359            .unwrap()
360            .dns_suffix(".github.com")
361            .http_host("github.com")
362            .build();
363
364        assert_eq!(app.name, "github");
365        assert_eq!(app.category, "developer");
366        assert_eq!(app.transports.len(), 1);
367        assert_eq!(app.dns_suffixes, vec![".github.com"]);
368        assert_eq!(app.http_hosts, vec!["github.com"]);
369    }
370}