1use std::collections::HashMap;
9
10use bytes::Bytes;
11
12use crate::message::SecurityLevel;
13use crate::oid::Oid;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum SecurityModel {
18 Any = 0,
20 V1 = 1,
22 V2c = 2,
24 Usm = 3,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub(crate) enum ContextMatch {
31 #[default]
33 Exact,
34 Prefix,
36}
37
38#[derive(Debug, Clone, Default)]
40pub struct View {
41 subtrees: Vec<ViewSubtree>,
42}
43
44impl View {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn include(mut self, oid: Oid) -> Self {
52 self.subtrees.push(ViewSubtree {
53 oid,
54 mask: Vec::new(),
55 included: true,
56 });
57 self
58 }
59
60 pub fn include_masked(mut self, oid: Oid, mask: Vec<u8>) -> Self {
62 self.subtrees.push(ViewSubtree {
63 oid,
64 mask,
65 included: true,
66 });
67 self
68 }
69
70 pub fn exclude(mut self, oid: Oid) -> Self {
72 self.subtrees.push(ViewSubtree {
73 oid,
74 mask: Vec::new(),
75 included: false,
76 });
77 self
78 }
79
80 pub fn exclude_masked(mut self, oid: Oid, mask: Vec<u8>) -> Self {
82 self.subtrees.push(ViewSubtree {
83 oid,
84 mask,
85 included: false,
86 });
87 self
88 }
89
90 pub fn contains(&self, oid: &Oid) -> bool {
96 let mut dominated_by_include = false;
97 let mut dominated_by_exclude = false;
98
99 for subtree in &self.subtrees {
100 if subtree.matches(oid) {
101 if subtree.included {
102 dominated_by_include = true;
103 } else {
104 dominated_by_exclude = true;
105 }
106 }
107 }
108
109 dominated_by_include && !dominated_by_exclude
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct ViewSubtree {
117 pub oid: Oid,
119 pub mask: Vec<u8>,
129 pub included: bool,
131}
132
133impl ViewSubtree {
134 pub fn matches(&self, oid: &Oid) -> bool {
136 let subtree_arcs = self.oid.arcs();
137 let oid_arcs = oid.arcs();
138
139 if oid_arcs.len() < subtree_arcs.len() {
141 return false;
142 }
143
144 for (i, &subtree_arc) in subtree_arcs.iter().enumerate() {
146 let mask_bit = if i / 8 < self.mask.len() {
147 (self.mask[i / 8] >> (7 - (i % 8))) & 1
148 } else {
149 1 };
151
152 if mask_bit == 1 && oid_arcs[i] != subtree_arc {
153 return false;
154 }
155 }
157
158 true
159 }
160}
161
162#[derive(Debug, Clone)]
164pub struct VacmAccessEntry {
165 pub group_name: Bytes,
167 pub context_prefix: Bytes,
169 pub security_model: SecurityModel,
171 pub security_level: SecurityLevel,
173 pub(crate) context_match: ContextMatch,
175 pub read_view: Bytes,
177 pub write_view: Bytes,
179 pub notify_view: Bytes,
181}
182
183pub struct AccessEntryBuilder {
185 group_name: Bytes,
186 context_prefix: Bytes,
187 security_model: SecurityModel,
188 security_level: SecurityLevel,
189 context_match: ContextMatch,
190 read_view: Bytes,
191 write_view: Bytes,
192 notify_view: Bytes,
193}
194
195impl AccessEntryBuilder {
196 pub fn new(group_name: impl Into<Bytes>) -> Self {
198 Self {
199 group_name: group_name.into(),
200 context_prefix: Bytes::new(),
201 security_model: SecurityModel::Any,
202 security_level: SecurityLevel::NoAuthNoPriv,
203 context_match: ContextMatch::Exact,
204 read_view: Bytes::new(),
205 write_view: Bytes::new(),
206 notify_view: Bytes::new(),
207 }
208 }
209
210 pub fn context_prefix(mut self, prefix: impl Into<Bytes>) -> Self {
212 self.context_prefix = prefix.into();
213 self
214 }
215
216 pub fn security_model(mut self, model: SecurityModel) -> Self {
218 self.security_model = model;
219 self
220 }
221
222 pub fn security_level(mut self, level: SecurityLevel) -> Self {
224 self.security_level = level;
225 self
226 }
227
228 pub fn context_match_prefix(mut self) -> Self {
234 self.context_match = ContextMatch::Prefix;
235 self
236 }
237
238 pub fn read_view(mut self, view: impl Into<Bytes>) -> Self {
240 self.read_view = view.into();
241 self
242 }
243
244 pub fn write_view(mut self, view: impl Into<Bytes>) -> Self {
246 self.write_view = view.into();
247 self
248 }
249
250 pub fn notify_view(mut self, view: impl Into<Bytes>) -> Self {
252 self.notify_view = view.into();
253 self
254 }
255
256 pub fn build(self) -> VacmAccessEntry {
258 VacmAccessEntry {
259 group_name: self.group_name,
260 context_prefix: self.context_prefix,
261 security_model: self.security_model,
262 security_level: self.security_level,
263 context_match: self.context_match,
264 read_view: self.read_view,
265 write_view: self.write_view,
266 notify_view: self.notify_view,
267 }
268 }
269}
270
271#[derive(Debug, Clone, Default)]
273pub struct VacmConfig {
274 security_to_group: HashMap<(SecurityModel, Bytes), Bytes>,
276 access_entries: Vec<VacmAccessEntry>,
278 views: HashMap<Bytes, View>,
280}
281
282impl VacmConfig {
283 pub fn new() -> Self {
285 Self::default()
286 }
287
288 pub fn add_group(
290 &mut self,
291 security_name: impl Into<Bytes>,
292 security_model: SecurityModel,
293 group_name: impl Into<Bytes>,
294 ) {
295 self.security_to_group
296 .insert((security_model, security_name.into()), group_name.into());
297 }
298
299 pub fn add_access(&mut self, entry: VacmAccessEntry) {
301 self.access_entries.push(entry);
302 }
303
304 pub fn add_view(&mut self, name: impl Into<Bytes>, view: View) {
306 self.views.insert(name.into(), view);
307 }
308
309 pub fn get_group(&self, model: SecurityModel, name: &[u8]) -> Option<&Bytes> {
311 let name_bytes = Bytes::copy_from_slice(name);
312 self.security_to_group
314 .get(&(model, name_bytes.clone()))
315 .or_else(|| {
317 self.security_to_group
318 .get(&(SecurityModel::Any, name_bytes))
319 })
320 }
321
322 pub fn get_access(
328 &self,
329 group: &[u8],
330 context: &[u8],
331 model: SecurityModel,
332 level: SecurityLevel,
333 ) -> Option<&VacmAccessEntry> {
334 self.access_entries
336 .iter()
337 .filter(|e| {
338 e.group_name.as_ref() == group
339 && self.context_matches(&e.context_prefix, context, e.context_match)
340 && (e.security_model == model || e.security_model == SecurityModel::Any)
341 && level >= e.security_level
342 })
343 .max_by_key(|e| {
344 let model_score = if e.security_model == model { 2 } else { 1 };
346 let context_score = e.context_prefix.len();
347 (model_score, context_score)
348 })
349 }
350
351 fn context_matches(&self, prefix: &[u8], context: &[u8], mode: ContextMatch) -> bool {
353 match mode {
354 ContextMatch::Exact => prefix == context,
355 ContextMatch::Prefix => context.starts_with(prefix),
356 }
357 }
358
359 pub fn check_access(&self, view_name: Option<&Bytes>, oid: &Oid) -> bool {
361 let Some(view_name) = view_name else {
362 return false;
363 };
364
365 if view_name.is_empty() {
366 return false;
367 }
368
369 let Some(view) = self.views.get(view_name) else {
370 return false;
371 };
372
373 view.contains(oid)
374 }
375}
376
377pub struct VacmBuilder {
379 config: VacmConfig,
380}
381
382impl VacmBuilder {
383 pub fn new() -> Self {
385 Self {
386 config: VacmConfig::new(),
387 }
388 }
389
390 pub fn group(
392 mut self,
393 security_name: impl Into<Bytes>,
394 security_model: SecurityModel,
395 group_name: impl Into<Bytes>,
396 ) -> Self {
397 self.config
398 .add_group(security_name, security_model, group_name);
399 self
400 }
401
402 pub fn access<F>(mut self, group_name: impl Into<Bytes>, configure: F) -> Self
404 where
405 F: FnOnce(AccessEntryBuilder) -> AccessEntryBuilder,
406 {
407 let builder = AccessEntryBuilder::new(group_name);
408 let entry = configure(builder).build();
409 self.config.add_access(entry);
410 self
411 }
412
413 pub fn view<F>(mut self, name: impl Into<Bytes>, configure: F) -> Self
415 where
416 F: FnOnce(View) -> View,
417 {
418 let view = configure(View::new());
419 self.config.add_view(name, view);
420 self
421 }
422
423 pub fn build(self) -> VacmConfig {
425 self.config
426 }
427}
428
429impl Default for VacmBuilder {
430 fn default() -> Self {
431 Self::new()
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::oid;
439
440 #[test]
441 fn test_view_contains_simple() {
442 let view = View::new().include(oid!(1, 3, 6, 1, 2, 1)); assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 0)));
446 assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1, 2, 1, 1)));
447
448 assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1)));
450
451 assert!(!view.contains(&oid!(1, 3, 6, 1, 4, 1)));
453 assert!(!view.contains(&oid!(1, 3, 6, 1, 2)));
454 }
455
456 #[test]
457 fn test_view_exclude() {
458 let view = View::new()
459 .include(oid!(1, 3, 6, 1, 2, 1)) .exclude(oid!(1, 3, 6, 1, 2, 1, 1, 7)); assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 0)));
464 assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)));
465
466 assert!(!view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 7)));
468 assert!(!view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 7, 0)));
469 }
470
471 #[test]
472 fn test_view_subtree_mask() {
473 let subtree = ViewSubtree {
479 oid: oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2), mask: vec![0xFF, 0xC0], included: true,
482 };
483
484 assert!(subtree.matches(&oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1)));
486 assert!(subtree.matches(&oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 999)));
487
488 assert!(!subtree.matches(&oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 3, 1)));
490 }
491
492 #[test]
493 fn test_vacm_group_lookup() {
494 let mut config = VacmConfig::new();
495 config.add_group("public", SecurityModel::V2c, "readonly_group");
496 config.add_group("admin", SecurityModel::Usm, "admin_group");
497
498 assert_eq!(
499 config.get_group(SecurityModel::V2c, b"public"),
500 Some(&Bytes::from_static(b"readonly_group"))
501 );
502 assert_eq!(
503 config.get_group(SecurityModel::Usm, b"admin"),
504 Some(&Bytes::from_static(b"admin_group"))
505 );
506 assert_eq!(config.get_group(SecurityModel::V1, b"public"), None);
507 }
508
509 #[test]
510 fn test_vacm_group_any_model() {
511 let mut config = VacmConfig::new();
512 config.add_group("universal", SecurityModel::Any, "universal_group");
513
514 assert_eq!(
516 config.get_group(SecurityModel::V1, b"universal"),
517 Some(&Bytes::from_static(b"universal_group"))
518 );
519 assert_eq!(
520 config.get_group(SecurityModel::V2c, b"universal"),
521 Some(&Bytes::from_static(b"universal_group"))
522 );
523 }
524
525 #[test]
526 fn test_vacm_access_lookup() {
527 let mut config = VacmConfig::new();
528 config.add_access(VacmAccessEntry {
529 group_name: Bytes::from_static(b"readonly_group"),
530 context_prefix: Bytes::new(),
531 security_model: SecurityModel::Any,
532 security_level: SecurityLevel::NoAuthNoPriv,
533 context_match: ContextMatch::Exact,
534 read_view: Bytes::from_static(b"full_view"),
535 write_view: Bytes::new(),
536 notify_view: Bytes::new(),
537 });
538
539 let access = config.get_access(
540 b"readonly_group",
541 b"",
542 SecurityModel::V2c,
543 SecurityLevel::NoAuthNoPriv,
544 );
545 assert!(access.is_some());
546 assert_eq!(access.unwrap().read_view, Bytes::from_static(b"full_view"));
547 }
548
549 #[test]
550 fn test_vacm_access_security_level() {
551 let mut config = VacmConfig::new();
552 config.add_access(VacmAccessEntry {
553 group_name: Bytes::from_static(b"admin_group"),
554 context_prefix: Bytes::new(),
555 security_model: SecurityModel::Usm,
556 security_level: SecurityLevel::AuthPriv, context_match: ContextMatch::Exact,
558 read_view: Bytes::from_static(b"full_view"),
559 write_view: Bytes::from_static(b"full_view"),
560 notify_view: Bytes::new(),
561 });
562
563 let access = config.get_access(
565 b"admin_group",
566 b"",
567 SecurityModel::Usm,
568 SecurityLevel::AuthNoPriv,
569 );
570 assert!(access.is_none());
571
572 let access = config.get_access(
574 b"admin_group",
575 b"",
576 SecurityModel::Usm,
577 SecurityLevel::AuthPriv,
578 );
579 assert!(access.is_some());
580 }
581
582 #[test]
583 fn test_vacm_check_access() {
584 let mut config = VacmConfig::new();
585 config.add_view("full_view", View::new().include(oid!(1, 3, 6, 1)));
586
587 assert!(config.check_access(
588 Some(&Bytes::from_static(b"full_view")),
589 &oid!(1, 3, 6, 1, 2, 1, 1, 0),
590 ));
591
592 assert!(!config.check_access(Some(&Bytes::new()), &oid!(1, 3, 6, 1, 2, 1, 1, 0),));
594
595 assert!(!config.check_access(None, &oid!(1, 3, 6, 1, 2, 1, 1, 0),));
597
598 assert!(!config.check_access(
600 Some(&Bytes::from_static(b"unknown_view")),
601 &oid!(1, 3, 6, 1, 2, 1, 1, 0),
602 ));
603 }
604
605 #[test]
606 fn test_vacm_builder() {
607 let config = VacmBuilder::new()
608 .group("public", SecurityModel::V2c, "readonly_group")
609 .group("admin", SecurityModel::Usm, "admin_group")
610 .access("readonly_group", |a| {
611 a.context_prefix("")
612 .security_model(SecurityModel::Any)
613 .security_level(SecurityLevel::NoAuthNoPriv)
614 .read_view("full_view")
615 })
616 .access("admin_group", |a| {
617 a.security_model(SecurityModel::Usm)
618 .security_level(SecurityLevel::AuthPriv)
619 .read_view("full_view")
620 .write_view("full_view")
621 })
622 .view("full_view", |v| v.include(oid!(1, 3, 6, 1)))
623 .build();
624
625 assert!(config.get_group(SecurityModel::V2c, b"public").is_some());
626 assert!(config.get_group(SecurityModel::Usm, b"admin").is_some());
627 }
628}