wick_config/lockdown/
port.rs

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  // Does this restriction include this component ID? If not, then we don't have access.
41  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  // If our template configuration is unrendered, there's a bug. Panic.
53  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}