1use async_trait::async_trait;
35use regex::Regex;
36use tracing::info;
37
38use super::policy::{Check, StageSet, Verdict};
39use super::{ConnectionContext, ListVerdict, check_lists, compile_anchored};
40
41#[derive(Debug, Clone, Default)]
43pub struct Settings {
44 pub allow: Vec<String>,
45 pub deny: Vec<String>,
46}
47
48#[derive(Debug)]
50pub struct PathList {
51 allow: Vec<Regex>,
52 deny: Vec<Regex>,
53}
54
55impl PathList {
56 pub fn from_settings(name: &str, settings: &Settings) -> anyhow::Result<Self> {
58 if settings.allow.is_empty() && settings.deny.is_empty() {
59 anyhow::bail!(
60 "filter.check.{name} has neither allow nor deny entries, so it would match \
61 every request; list the paths it is about, or drop the check"
62 );
63 }
64
65 let check = Self {
66 allow: compile_paths(&settings.allow, name, "allow")?,
67 deny: compile_paths(&settings.deny, name, "deny")?,
68 };
69 info!(event = "filter_path_loaded", outcome = "success", check = name, allow = ?settings.allow, deny = ?settings.deny);
70 Ok(check)
71 }
72}
73
74fn compile_paths(globs: &[String], check: &str, side: &str) -> anyhow::Result<Vec<Regex>> {
80 let patterns: Vec<String> = globs
81 .iter()
82 .map(|glob| {
83 glob.split('*')
84 .map(regex::escape)
85 .collect::<Vec<_>>()
86 .join("[^/]+")
87 })
88 .collect();
89 compile_anchored(&patterns, &format!("filter.check.{check}.{side}"))
90}
91
92#[async_trait]
93impl Check for PathList {
94 fn kind(&self) -> &'static str {
95 "path"
96 }
97
98 fn stages(&self) -> StageSet {
102 StageSet::connection_only()
103 }
104
105 async fn check_connection(&self, context: &ConnectionContext<'_>) -> Verdict {
106 let path = context.path;
107 match check_lists(&self.allow, &self.deny, |pattern| pattern.is_match(path)) {
108 ListVerdict::Permitted => Verdict::Pass,
109 ListVerdict::Denied => Verdict::Fail(format!("path {path} is denied")),
110 ListVerdict::NotAllowed => Verdict::Fail(format!("path {path} is not allowed")),
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use axum::http::Method;
119
120 fn settings(allow: &[&str], deny: &[&str]) -> Settings {
121 Settings {
122 allow: allow.iter().map(std::string::ToString::to_string).collect(),
123 deny: deny.iter().map(std::string::ToString::to_string).collect(),
124 }
125 }
126
127 fn built(allow: &[&str], deny: &[&str]) -> PathList {
128 PathList::from_settings("paths", &settings(allow, deny)).unwrap()
129 }
130
131 async fn verdict_for(check: &PathList, path: &str) -> Verdict {
132 check
133 .check_connection(&ConnectionContext {
134 client_ip: Some("10.0.0.5".parse().unwrap()),
135 method: &Method::GET,
136 path,
137 })
138 .await
139 }
140
141 #[tokio::test]
142 async fn an_exact_path_matches_only_itself() {
143 let check = built(&["/crl"], &[]);
144 assert_eq!(verdict_for(&check, "/crl").await, Verdict::Pass);
145 assert!(matches!(
146 verdict_for(&check, "/crl/extra").await,
147 Verdict::Fail(_)
148 ));
149 assert!(matches!(
150 verdict_for(&check, "/directory").await,
151 Verdict::Fail(_)
152 ));
153 }
154
155 #[tokio::test]
158 async fn a_glob_star_matches_exactly_one_path_segment() {
159 let check = built(&["/renewalInfo/*"], &[]);
160 assert_eq!(
161 verdict_for(&check, "/renewalInfo/abc123").await,
162 Verdict::Pass
163 );
164 assert!(matches!(
165 verdict_for(&check, "/renewalInfo/abc/def").await,
166 Verdict::Fail(_)
167 ));
168 assert!(matches!(
169 verdict_for(&check, "/renewalInfo/").await,
170 Verdict::Fail(_)
171 ));
172 }
173
174 #[tokio::test]
175 async fn deny_wins_and_names_which_list_bit() {
176 let check = built(&["/*"], &["/revokeCert"]);
177 assert_eq!(verdict_for(&check, "/directory").await, Verdict::Pass);
178 match verdict_for(&check, "/revokeCert").await {
179 Verdict::Fail(detail) => assert!(detail.contains("is denied"), "{detail}"),
180 other => panic!("expected Fail, got {other:?}"),
181 }
182
183 let allow_only = built(&["/crl"], &[]);
184 match verdict_for(&allow_only, "/newOrder").await {
185 Verdict::Fail(detail) => assert!(detail.contains("is not allowed"), "{detail}"),
186 other => panic!("expected Fail, got {other:?}"),
187 }
188 }
189
190 #[tokio::test]
191 async fn a_deny_only_check_permits_everything_else() {
192 let check = built(&[], &["/revokeCert"]);
193 assert_eq!(verdict_for(&check, "/directory").await, Verdict::Pass);
194 assert!(matches!(
195 verdict_for(&check, "/revokeCert").await,
196 Verdict::Fail(_)
197 ));
198 }
199
200 #[tokio::test]
203 async fn every_other_character_is_literal() {
204 let check = built(&["/a.b"], &[]);
205 assert_eq!(verdict_for(&check, "/a.b").await, Verdict::Pass);
206 assert!(matches!(
207 verdict_for(&check, "/axb").await,
208 Verdict::Fail(_)
209 ));
210 }
211
212 #[test]
213 fn an_empty_check_is_a_startup_error() {
214 let error = PathList::from_settings("paths", &Settings::default())
215 .unwrap_err()
216 .to_string();
217 assert!(error.contains("would match every request"), "{error}");
218 assert!(error.contains("filter.check.paths"), "{error}");
219 }
220
221 #[test]
222 fn reports_its_type_and_stages() {
223 let check = built(&["/crl"], &[]);
224 assert_eq!(check.kind(), "path");
225 assert_eq!(check.stages(), StageSet::connection_only());
226 }
227}