1#![allow(dead_code)]
2use std::collections::HashSet;
3
4use wildmatch::WildMatch;
5
6use super::{FailureKind, LockdownError};
7use crate::audit::AuditedPort;
8use crate::config::resources::ResourceKind;
9use crate::config::PortRestriction;
10
11pub(crate) fn validate<'a>(
12 component_id: &str,
13 resource_id: &str,
14 resource: &AuditedPort,
15 restrictions: impl Iterator<Item = &'a PortRestriction>,
16) -> Result<(), LockdownError> {
17 let mut failures = HashSet::new();
18 for restriction in restrictions {
19 match is_allowed(component_id, resource_id, resource, restriction) {
20 Ok(_) => return Ok(()),
21 Err(e) => {
22 failures.insert(e);
23 }
24 }
25 }
26
27 if failures.is_empty() {
28 Ok(())
29 } else {
30 Err(LockdownError::new(failures.into_iter().collect()))
31 }
32}
33
34pub(crate) fn is_allowed(
35 component_id: &str,
36 _resource_id: &str,
37 resource: &AuditedPort,
38 restriction: &PortRestriction,
39) -> Result<(), FailureKind> {
40 if !restriction
42 .components()
43 .iter()
44 .any(|c| WildMatch::new(c).matches(component_id))
45 {
46 return Err(FailureKind::NotExpresslyAllowed(
47 component_id.to_owned(),
48 ResourceKind::TcpPort,
49 ));
50 }
51
52 let (Some(port_restriction), Some(host_restriction)) = (restriction.port().value(), restriction.address().value())
54 else {
55 panic!("port restriction's allow template is unrendered");
56 };
57
58 let port_str = resource.port.to_string();
59
60 if !WildMatch::new(port_restriction).matches(&port_str) {
61 return Err(FailureKind::Port(component_id.to_owned(), resource.port));
62 }
63
64 if !WildMatch::new(host_restriction).matches(&resource.address) {
65 return Err(FailureKind::Address(component_id.to_owned(), resource.address.clone()));
66 }
67
68 Ok(())
69}
70
71#[cfg(test)]
72mod test {
73 use anyhow::Result;
74 use wick_logger::LoggingOptions;
75
76 use super::*;
77 use crate::config::template_config::Renderable;
78
79 #[rstest::rstest]
80 #[case("test", "0.0.0.0", 80,(["test"], "*","*"), )]
81 #[case("test", "0.0.0.0", 80,(["test"], "*","80"), )]
82 #[case("test", "0.0.0.0", 80,(["test"], "0.0.0.0","*"), )]
83 fn test_allowed<const K: usize>(
84 #[case] component_id: &str,
85 #[case] desired_address: &str,
86 #[case] desired_port: u16,
87 #[case] restriction: ([&str; K], &str, &str),
88 ) -> Result<()> {
89 let _ = wick_logger::init_test(&LoggingOptions::with_level(wick_logger::LogLevel::Trace));
90 let volume = AuditedPort {
91 port: desired_port,
92 address: desired_address.into(),
93 };
94 let mut restriction = PortRestriction::new_from_template(
95 restriction.0.into_iter().map(Into::into).collect::<Vec<_>>(),
96 restriction.1,
97 restriction.2,
98 );
99 restriction.render_config(None, None, None)?;
100 assert_eq!(
101 is_allowed(component_id, "ID", &volume, &restriction),
102 Ok(()),
103 "component {} should have access to {}:{}",
104 component_id,
105 desired_address,
106 desired_port,
107 );
108
109 Ok(())
110 }
111
112 #[rstest::rstest]
113 #[case("test", "0.0.0.0", 80, (["test"], "0.0.0.0","8080"), FailureKind::Port("test".into(), 80))]
114 #[case("test", "0.0.0.0", 80, (["test"], "127.0.0.1","80"), FailureKind::Address("test".into(), "0.0.0.0".into()))]
115 fn test_restricted<const K: usize>(
116 #[case] component_id: &str,
117 #[case] desired_address: &str,
118 #[case] desired_port: u16,
119 #[case] restriction: ([&str; K], &str, &str),
120 #[case] failure: FailureKind,
121 ) -> Result<()> {
122 let _ = wick_logger::init_test(&LoggingOptions::with_level(wick_logger::LogLevel::Trace));
123 let volume = AuditedPort {
124 port: desired_port,
125 address: desired_address.into(),
126 };
127 let mut restriction = PortRestriction::new_from_template(
128 restriction.0.into_iter().map(Into::into).collect::<Vec<_>>(),
129 restriction.1,
130 restriction.2,
131 );
132 restriction.render_config(None, None, None)?;
133 assert_eq!(
134 is_allowed(component_id, "ID", &volume, &restriction),
135 Err(failure),
136 "component {} should not have access to {}:{}",
137 component_id,
138 desired_address,
139 desired_port
140 );
141
142 Ok(())
143 }
144}