1use crate::config::*;
29use erbium_net::addr::{NetAddr, NetAddrExt as _, UNSPECIFIED6, WithPort as _};
30use yaml_rust::yaml;
31
32#[derive(Debug)]
33pub struct Permission {
34 pub allow_dns_recursion: bool,
35 pub allow_http: bool,
36 pub allow_http_metrics: bool,
37 pub allow_http_leases: bool,
38}
39
40pub struct Attributes {
41 pub addr: NetAddr,
42}
43
44impl std::fmt::Display for Attributes {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(f, "{}", self.addr)
47 }
48}
49
50impl Default for Attributes {
53 fn default() -> Self {
54 Self {
55 addr: UNSPECIFIED6.with_port(0),
56 }
57 }
58}
59
60#[derive(Debug)]
61pub struct Acl {
62 pub subnet: Option<Vec<Prefix>>,
63 pub unix: Option<bool>,
64 pub permission: Permission,
65}
66
67fn check_subnet(attr: &Attributes, prefix: &Prefix) -> bool {
68 match attr.addr.ip() {
69 Some(sockaddr) => prefix.contains(sockaddr),
70 _ => false,
71 }
72}
73
74impl Acl {
75 fn check(&self, attr: &Attributes) -> Option<&'_ Permission> {
76 let mut ok = true;
77 ok = ok
79 && self
80 .subnet
81 .as_ref()
82 .map(|ss| ss.iter().any(|s| check_subnet(attr, s)))
83 .unwrap_or(true);
84 if let Some(unix) = self.unix {
86 ok = ok && attr.addr.as_unix_addr().is_some() == unix;
87 }
88
89 if ok { Some(&self.permission) } else { None }
90 }
91}
92
93pub enum PermissionType {
94 DnsRecursion,
95 Http,
96 HttpLeases,
97 HttpMetrics,
98}
99
100impl std::fmt::Display for PermissionType {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 use PermissionType::*;
103 match self {
104 DnsRecursion => write!(f, "DNS Recursion"),
105 Http => write!(f, "HTTP"),
106 HttpLeases => write!(f, "HTTP Leases"),
107 HttpMetrics => write!(f, "HTTP Metrics"),
108 }
109 }
110}
111
112#[cfg_attr(test, derive(Debug))]
113#[derive(Eq, PartialEq)]
114pub enum AclError {
115 NotAuthenticated,
116 NotAuthorised(String),
117}
118
119impl std::fmt::Display for AclError {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 use AclError::*;
122 match self {
123 NotAuthenticated => write!(f, "Failed to match any ACLs"),
124 NotAuthorised(perm) => write!(f, "Matched ACL does not have permission {}", perm),
125 }
126 }
127}
128
129fn check_authenticated<'v>(acl: &'v [Acl], attr: &Attributes) -> Result<&'v Permission, AclError> {
133 acl.iter()
134 .find_map(|a| a.check(attr))
135 .ok_or(AclError::NotAuthenticated)
136}
137
138pub fn require_permission(
142 acl: &[Acl],
143 client: &Attributes,
144 perm: PermissionType,
145) -> Result<(), AclError> {
146 fn check_permission(perm: bool, name: &str) -> Result<(), AclError> {
147 if perm {
148 Ok(())
149 } else {
150 Err(AclError::NotAuthorised(name.into()))
151 }
152 }
153 use PermissionType::*;
154 match (check_authenticated(acl, client), perm) {
155 (Ok(perms), DnsRecursion) => check_permission(perms.allow_dns_recursion, "dns-recursion"),
156 (Ok(perms), Http) => check_permission(perms.allow_http, "http"),
157 (Ok(perms), HttpLeases) => check_permission(perms.allow_http_leases, "http-leases"),
158 (Ok(perms), HttpMetrics) => check_permission(perms.allow_http_metrics, "http-metrics"),
159 (Err(err), perm) => {
160 log::warn!("{}: {}: {}", client, perm, err);
161 Err(err)
162 }
163 }
164}
165
166pub fn default_acls(addresses: &[Prefix]) -> Vec<Acl> {
167 vec![
168 Acl {
169 subnet: Some(addresses.to_vec()),
171 unix: None,
172 permission: Permission {
173 allow_dns_recursion: true,
174 allow_http_leases: true,
175 allow_http_metrics: true,
176 allow_http: true,
177 },
178 },
179 Acl {
180 subnet: Some(vec![
182 Prefix::V4(Prefix4 {
183 addr: "127.0.0.0".parse().unwrap(),
184 prefixlen: 8,
185 }),
186 Prefix::V6(Prefix6 {
187 addr: "::1".parse().unwrap(),
188 prefixlen: 128,
189 }),
190 ]),
191 unix: None,
192 permission: Permission {
193 allow_dns_recursion: true,
194 allow_http_leases: true,
195 allow_http_metrics: true,
196 allow_http: true,
197 },
198 },
199 Acl {
200 subnet: None,
202 unix: Some(true),
203 permission: Permission {
204 allow_dns_recursion: false,
205 allow_http_leases: true,
206 allow_http_metrics: true,
207 allow_http: true,
208 },
209 },
210 ]
211}
212
213pub(crate) fn parse_acl(name: &str, fragment: &yaml::Yaml) -> Result<Option<Acl>, Error> {
214 match fragment {
215 yaml::Yaml::Hash(h) => {
216 let mut subnet = None;
217 let mut unix = None;
218 let mut accesses = vec![];
219 for (k, v) in h {
220 match (k.as_str(), v) {
221 (Some("match-subnets"), s) => {
222 subnet = parse_array("match-subnets", s, parse_string_prefix)?;
223 }
224 (Some("match-unix"), s) => {
225 unix = parse_boolean("match-unix", s)?;
226 }
227 (Some("apply-access"), a) => {
228 accesses =
229 parse_array("apply-access", a, parse_string)?.ok_or_else(|| {
230 Error::InvalidConfig("apply-access cannot be null".into())
231 })?;
232 }
233 (Some(m), _) => {
234 return Err(Error::InvalidConfig(format!("Unknown {} key {}", name, m)));
235 }
236 (None, _) => {
237 return Err(Error::InvalidConfig(format!(
238 "{} keys are expected to be strings",
239 name
240 )));
241 }
242 }
243 }
244 let mut allow_dns_recursion = false;
245 let mut allow_http = false;
246 let mut allow_http_metrics = false;
247 let mut allow_http_leases = false;
248 for access in accesses {
249 match access.as_str() {
250 "dhcp-client" => {
251 allow_dns_recursion = true;
252 }
253 "dns-recursion" => allow_dns_recursion = true,
254 "http" => allow_http = true,
255 "http-metrics" => allow_http_metrics = true,
256 "http-leases" => allow_http_leases = true,
257 "http-ro" => {
258 allow_http = true;
259 allow_http_metrics = true;
260 allow_http_leases = true;
261 }
262 e => return Err(Error::InvalidConfig(format!("Unknown access {}", e))),
263 }
264 }
265 Ok(Some(Acl {
266 subnet,
267 unix,
268 permission: Permission {
269 allow_dns_recursion,
270 allow_http,
271 allow_http_metrics,
272 allow_http_leases,
273 },
274 }))
275 }
276 e => Err(Error::InvalidConfig(format!(
277 "Expected hash for {}, got {}",
278 name,
279 type_to_name(e)
280 ))),
281 }
282}
283
284#[test]
285fn acl_not_authenticated() {
286 use erbium_net::addr::{Ipv4Addr, ToNetAddr as _, WithPort as _};
287 let test_acls = vec![Acl {
288 subnet: Some(vec![Prefix::V4(Prefix4 {
289 addr: "192.0.2.0".parse().unwrap(),
290 prefixlen: 24,
291 })]),
292 unix: None,
293 permission: Permission {
294 allow_dns_recursion: true,
295 allow_http: false,
296 allow_http_leases: false,
297 allow_http_metrics: false,
298 },
299 }];
300
301 let ip = "192.168.0.1".parse::<Ipv4Addr>().unwrap().with_port(0);
302
303 let client = Attributes {
304 addr: ip.to_net_addr(),
305 };
306
307 assert_eq!(
308 require_permission(&test_acls, &client, PermissionType::DnsRecursion)
309 .expect_err("Unexpected succeeded")
310 .to_string(),
311 "Failed to match any ACLs"
312 );
313}
314
315#[test]
316fn acl_not_authorized() {
317 use erbium_net::addr::{Ipv4Addr, ToNetAddr as _, WithPort as _};
318 let test_acls = vec![Acl {
319 subnet: Some(vec![Prefix::V4(Prefix4 {
320 addr: "192.0.2.0".parse().unwrap(),
321 prefixlen: 24,
322 })]),
323 unix: None,
324 permission: Permission {
325 allow_dns_recursion: false,
326 allow_http: false,
327 allow_http_leases: false,
328 allow_http_metrics: false,
329 },
330 }];
331
332 let ip = "192.0.2.1".parse::<Ipv4Addr>().unwrap().with_port(0);
333
334 let client = Attributes {
335 addr: ip.to_net_addr(),
336 };
337
338 assert_eq!(
339 require_permission(&test_acls, &client, PermissionType::DnsRecursion)
340 .expect_err("Unexpected success")
341 .to_string(),
342 "Matched ACL does not have permission dns-recursion"
343 );
344}
345
346#[test]
347fn acl_allowed() {
348 use erbium_net::addr::{Ipv4Addr, ToNetAddr as _, WithPort as _};
349 let test_acls = vec![Acl {
350 subnet: Some(vec![Prefix::V4(Prefix4 {
351 addr: "192.0.2.0".parse().unwrap(),
352 prefixlen: 24,
353 })]),
354 unix: None,
355 permission: Permission {
356 allow_dns_recursion: true,
357 allow_http: false,
358 allow_http_leases: false,
359 allow_http_metrics: false,
360 },
361 }];
362
363 let ip = "192.0.2.1".parse::<Ipv4Addr>().unwrap().with_port(0);
364
365 let client = Attributes {
366 addr: ip.to_net_addr(),
367 };
368
369 assert_eq!(
370 require_permission(&test_acls, &client, PermissionType::DnsRecursion),
371 Ok(())
372 );
373}
374
375#[test]
376fn acl_parse() {
377 load_config_from_string_for_test(
378 "---
379 acls:
380 - match-subnets: [192.0.2.0/24]
381 apply-access: ['dns-recursion']
382 ",
383 )
384 .expect("Failed to parse ACL configuration");
385}
386
387#[test]
388fn acl_parse_fail() {
389 assert_eq!(
390 load_config_from_string_for_test(
391 "---
392 acls:
393 - match-subnets: [192.0.2.0/24]
394 apply-access: ['not-a-permission']
395 ",
396 )
397 .expect_err("Bad config unexpectedly successfully parsed")
398 .to_string(),
399 "Invalid Configuration: Unknown access not-a-permission"
400 );
401
402 assert_eq!(
403 load_config_from_string_for_test(
404 "---
405 acls:
406 - not-a-valid-key: 192.0.2.0/24
407 ",
408 )
409 .expect_err("Bad config unexpectedly successfully parsed")
410 .to_string(),
411 "Invalid Configuration: Unknown acls key not-a-valid-key"
412 );
413
414 assert_eq!(
415 load_config_from_string_for_test(
416 "---
417 acls:
418 - match-subnets: [192.0.2.0/24]
419 apply-access: null
420 ",
421 )
422 .expect_err("Bad config unexpectedly successfully parsed")
423 .to_string(),
424 "Invalid Configuration: apply-access cannot be null"
425 );
426}