use std::collections::HashMap;
use bytes::Bytes;
use crate::message::SecurityLevel;
use crate::oid::Oid;
pub use crate::handler::SecurityModel;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum ContextMatch {
#[default]
Exact,
Prefix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewCheckResult {
Included,
Excluded,
Ambiguous,
}
#[derive(Debug, Clone, Default)]
pub struct View {
subtrees: Vec<ViewSubtree>,
}
impl View {
pub fn new() -> Self {
Self::default()
}
pub fn include(mut self, oid: Oid) -> Self {
self.subtrees.push(ViewSubtree {
oid,
mask: Vec::new(),
included: true,
});
self
}
pub fn include_masked(mut self, oid: Oid, mask: Vec<u8>) -> Self {
self.subtrees.push(ViewSubtree {
oid,
mask,
included: true,
});
self
}
pub fn exclude(mut self, oid: Oid) -> Self {
self.subtrees.push(ViewSubtree {
oid,
mask: Vec::new(),
included: false,
});
self
}
pub fn exclude_masked(mut self, oid: Oid, mask: Vec<u8>) -> Self {
self.subtrees.push(ViewSubtree {
oid,
mask,
included: false,
});
self
}
pub fn contains(&self, oid: &Oid) -> bool {
let mut best_len: Option<usize> = None;
let mut best_included = false;
for subtree in &self.subtrees {
if subtree.matches(oid) {
let len = subtree.oid.len();
match best_len {
Some(prev) if len < prev => {}
Some(prev) if len == prev && !subtree.included => {
best_included = false;
}
_ => {
best_len = Some(len);
best_included = subtree.included;
}
}
}
}
best_included
}
pub fn check_subtree(&self, oid: &Oid) -> ViewCheckResult {
let mut best_covering_len: Option<usize> = None;
let mut best_covering_included = false;
let mut has_child_include = false;
let mut has_child_exclude = false;
let query_arcs = oid.arcs();
for subtree in &self.subtrees {
if subtree.matches(oid) {
let len = subtree.oid.len();
match best_covering_len {
Some(prev) if len < prev => {}
Some(prev) if len == prev && !subtree.included => {
best_covering_included = false;
}
_ => {
best_covering_len = Some(len);
best_covering_included = subtree.included;
}
}
}
let subtree_arcs = subtree.oid.arcs();
if subtree_arcs.len() > query_arcs.len()
&& subtree_arcs[..query_arcs.len()] == *query_arcs
{
if subtree.included {
has_child_include = true;
} else {
has_child_exclude = true;
}
}
}
match (best_covering_len.is_some(), best_covering_included) {
(true, false) => {
if has_child_include {
return ViewCheckResult::Ambiguous;
}
return ViewCheckResult::Excluded;
}
(true, true) => {
if has_child_exclude {
return ViewCheckResult::Ambiguous;
}
return ViewCheckResult::Included;
}
_ => {}
}
if has_child_include {
return ViewCheckResult::Ambiguous;
}
ViewCheckResult::Excluded
}
}
#[derive(Debug, Clone)]
pub struct ViewSubtree {
pub oid: Oid,
pub mask: Vec<u8>,
pub included: bool,
}
impl ViewSubtree {
pub fn matches(&self, oid: &Oid) -> bool {
let subtree_arcs = self.oid.arcs();
let oid_arcs = oid.arcs();
if oid_arcs.len() < subtree_arcs.len() {
return false;
}
for (i, &subtree_arc) in subtree_arcs.iter().enumerate() {
let mask_bit = if i / 8 < self.mask.len() {
(self.mask[i / 8] >> (7 - (i % 8))) & 1
} else {
1 };
if mask_bit == 1 && oid_arcs[i] != subtree_arc {
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
pub struct VacmAccessEntry {
pub group_name: Bytes,
pub context_prefix: Bytes,
pub security_model: SecurityModel,
pub security_level: SecurityLevel,
pub(crate) context_match: ContextMatch,
pub read_view: Bytes,
pub write_view: Bytes,
pub notify_view: Bytes,
}
pub struct AccessEntryBuilder {
group_name: Bytes,
context_prefix: Bytes,
security_model: SecurityModel,
security_level: SecurityLevel,
context_match: ContextMatch,
read_view: Bytes,
write_view: Bytes,
notify_view: Bytes,
}
impl AccessEntryBuilder {
pub fn new(group_name: impl Into<Bytes>) -> Self {
Self {
group_name: group_name.into(),
context_prefix: Bytes::new(),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::new(),
write_view: Bytes::new(),
notify_view: Bytes::new(),
}
}
pub fn context_prefix(mut self, prefix: impl Into<Bytes>) -> Self {
self.context_prefix = prefix.into();
self
}
pub fn security_model(mut self, model: SecurityModel) -> Self {
self.security_model = model;
self
}
pub fn security_level(mut self, level: SecurityLevel) -> Self {
self.security_level = level;
self
}
pub fn context_match_prefix(mut self) -> Self {
self.context_match = ContextMatch::Prefix;
self
}
pub fn read_view(mut self, view: impl Into<Bytes>) -> Self {
self.read_view = view.into();
self
}
pub fn write_view(mut self, view: impl Into<Bytes>) -> Self {
self.write_view = view.into();
self
}
pub fn notify_view(mut self, view: impl Into<Bytes>) -> Self {
self.notify_view = view.into();
self
}
pub fn build(self) -> VacmAccessEntry {
VacmAccessEntry {
group_name: self.group_name,
context_prefix: self.context_prefix,
security_model: self.security_model,
security_level: self.security_level,
context_match: self.context_match,
read_view: self.read_view,
write_view: self.write_view,
notify_view: self.notify_view,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct VacmConfig {
security_to_group: HashMap<(SecurityModel, Bytes), Bytes>,
access_entries: Vec<VacmAccessEntry>,
views: HashMap<Bytes, View>,
}
impl VacmConfig {
pub fn new() -> Self {
Self::default()
}
pub fn add_group(
&mut self,
security_name: impl Into<Bytes>,
security_model: SecurityModel,
group_name: impl Into<Bytes>,
) {
self.security_to_group
.insert((security_model, security_name.into()), group_name.into());
}
pub fn add_access(&mut self, entry: VacmAccessEntry) {
self.access_entries.push(entry);
}
pub fn add_view(&mut self, name: impl Into<Bytes>, view: View) {
self.views.insert(name.into(), view);
}
pub fn get_group(&self, model: SecurityModel, name: &[u8]) -> Option<&Bytes> {
let mut any_match = None;
for ((entry_model, entry_name), group) in &self.security_to_group {
if entry_name.as_ref() == name {
if *entry_model == model {
return Some(group);
} else if *entry_model == SecurityModel::Any {
any_match = Some(group);
}
}
}
any_match
}
pub fn get_access(
&self,
group: &[u8],
context: &[u8],
model: SecurityModel,
level: SecurityLevel,
) -> Option<&VacmAccessEntry> {
self.access_entries
.iter()
.filter(|e| {
e.group_name.as_ref() == group
&& self.context_matches(&e.context_prefix, context, e.context_match)
&& (e.security_model == model || e.security_model == SecurityModel::Any)
&& level >= e.security_level
})
.max_by_key(|e| {
let model_score: u8 = if e.security_model == model { 1 } else { 0 };
let match_score: u8 = if e.context_match == ContextMatch::Exact {
1
} else {
0
};
let prefix_len = e.context_prefix.len();
let level_score = e.security_level as u8;
(model_score, match_score, prefix_len, level_score)
})
}
fn context_matches(&self, prefix: &[u8], context: &[u8], mode: ContextMatch) -> bool {
match mode {
ContextMatch::Exact => prefix == context,
ContextMatch::Prefix => context.starts_with(prefix),
}
}
pub fn check_access(&self, view_name: Option<&Bytes>, oid: &Oid) -> bool {
let Some(view_name) = view_name else {
return false;
};
if view_name.is_empty() {
return false;
}
let Some(view) = self.views.get(view_name) else {
return false;
};
view.contains(oid)
}
}
pub struct VacmBuilder {
config: VacmConfig,
}
impl VacmBuilder {
pub fn new() -> Self {
Self {
config: VacmConfig::new(),
}
}
pub fn group(
mut self,
security_name: impl Into<Bytes>,
security_model: SecurityModel,
group_name: impl Into<Bytes>,
) -> Self {
self.config
.add_group(security_name, security_model, group_name);
self
}
pub fn access<F>(mut self, group_name: impl Into<Bytes>, configure: F) -> Self
where
F: FnOnce(AccessEntryBuilder) -> AccessEntryBuilder,
{
let builder = AccessEntryBuilder::new(group_name);
let entry = configure(builder).build();
self.config.add_access(entry);
self
}
pub fn view<F>(mut self, name: impl Into<Bytes>, configure: F) -> Self
where
F: FnOnce(View) -> View,
{
let view = configure(View::new());
self.config.add_view(name, view);
self
}
pub fn build(self) -> VacmConfig {
self.config
}
}
impl Default for VacmBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oid;
#[test]
fn test_view_contains_simple() {
let view = View::new().include(oid!(1, 3, 6, 1, 2, 1));
assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 0)));
assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1, 2, 1, 1)));
assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1)));
assert!(!view.contains(&oid!(1, 3, 6, 1, 4, 1)));
assert!(!view.contains(&oid!(1, 3, 6, 1, 2)));
}
#[test]
fn test_view_exclude() {
let view = View::new()
.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)));
assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)));
assert!(!view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 7)));
assert!(!view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 7, 0)));
}
#[test]
fn test_view_longest_match_wins() {
let view = View::new()
.include(oid!(1, 3, 6, 1))
.exclude(oid!(1, 3, 6, 1, 2))
.include(oid!(1, 3, 6, 1, 2, 1));
assert!(view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 0)));
assert!(!view.contains(&oid!(1, 3, 6, 1, 2, 3, 1, 0)));
assert!(view.contains(&oid!(1, 3, 6, 1, 4, 1, 0)));
}
#[test]
fn test_view_longest_match_exclude_wins() {
let view = View::new()
.include(oid!(1, 3, 6, 1))
.exclude(oid!(1, 3, 6, 1, 2, 1));
assert!(!view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 0)));
assert!(view.contains(&oid!(1, 3, 6, 1, 4, 1, 0)));
}
#[test]
fn test_view_equal_length_exclude_wins() {
let view = View::new()
.include(oid!(1, 3, 6, 1, 2, 1))
.exclude(oid!(1, 3, 6, 1, 2, 1));
assert!(!view.contains(&oid!(1, 3, 6, 1, 2, 1, 1, 0)));
}
#[test]
fn test_view_subtree_mask() {
let subtree = ViewSubtree {
oid: oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2), mask: vec![0xFF, 0xC0], included: true,
};
assert!(subtree.matches(&oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1)));
assert!(subtree.matches(&oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 999)));
assert!(!subtree.matches(&oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 3, 1)));
}
#[test]
fn test_vacm_group_lookup() {
let mut config = VacmConfig::new();
config.add_group("public", SecurityModel::V2c, "readonly_group");
config.add_group("admin", SecurityModel::Usm, "admin_group");
assert_eq!(
config.get_group(SecurityModel::V2c, b"public"),
Some(&Bytes::from_static(b"readonly_group"))
);
assert_eq!(
config.get_group(SecurityModel::Usm, b"admin"),
Some(&Bytes::from_static(b"admin_group"))
);
assert_eq!(config.get_group(SecurityModel::V1, b"public"), None);
}
#[test]
fn test_vacm_group_any_model() {
let mut config = VacmConfig::new();
config.add_group("universal", SecurityModel::Any, "universal_group");
assert_eq!(
config.get_group(SecurityModel::V1, b"universal"),
Some(&Bytes::from_static(b"universal_group"))
);
assert_eq!(
config.get_group(SecurityModel::V2c, b"universal"),
Some(&Bytes::from_static(b"universal_group"))
);
}
#[test]
fn test_vacm_access_lookup() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"readonly_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"full_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config.get_access(
b"readonly_group",
b"",
SecurityModel::V2c,
SecurityLevel::NoAuthNoPriv,
);
assert!(access.is_some());
assert_eq!(access.unwrap().read_view, Bytes::from_static(b"full_view"));
}
#[test]
fn test_vacm_access_security_level() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"admin_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::Usm,
security_level: SecurityLevel::AuthPriv, context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"full_view"),
write_view: Bytes::from_static(b"full_view"),
notify_view: Bytes::new(),
});
let access = config.get_access(
b"admin_group",
b"",
SecurityModel::Usm,
SecurityLevel::AuthNoPriv,
);
assert!(access.is_none());
let access = config.get_access(
b"admin_group",
b"",
SecurityModel::Usm,
SecurityLevel::AuthPriv,
);
assert!(access.is_some());
}
#[test]
fn test_vacm_check_access() {
let mut config = VacmConfig::new();
config.add_view("full_view", View::new().include(oid!(1, 3, 6, 1)));
assert!(config.check_access(
Some(&Bytes::from_static(b"full_view")),
&oid!(1, 3, 6, 1, 2, 1, 1, 0),
));
assert!(!config.check_access(Some(&Bytes::new()), &oid!(1, 3, 6, 1, 2, 1, 1, 0),));
assert!(!config.check_access(None, &oid!(1, 3, 6, 1, 2, 1, 1, 0),));
assert!(!config.check_access(
Some(&Bytes::from_static(b"unknown_view")),
&oid!(1, 3, 6, 1, 2, 1, 1, 0),
));
}
#[test]
fn test_vacm_builder() {
let config = VacmBuilder::new()
.group("public", SecurityModel::V2c, "readonly_group")
.group("admin", SecurityModel::Usm, "admin_group")
.access("readonly_group", |a| {
a.context_prefix("")
.security_model(SecurityModel::Any)
.security_level(SecurityLevel::NoAuthNoPriv)
.read_view("full_view")
})
.access("admin_group", |a| {
a.security_model(SecurityModel::Usm)
.security_level(SecurityLevel::AuthPriv)
.read_view("full_view")
.write_view("full_view")
})
.view("full_view", |v| v.include(oid!(1, 3, 6, 1)))
.build();
assert!(config.get_group(SecurityModel::V2c, b"public").is_some());
assert!(config.get_group(SecurityModel::Usm, b"admin").is_some());
}
#[test]
fn test_vacm_access_prefers_specific_security_model_over_any() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"any_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::V2c,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"v2c_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"",
SecurityModel::V2c,
SecurityLevel::NoAuthNoPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"v2c_view"),
"should prefer specific security model over Any"
);
}
#[test]
fn test_vacm_access_prefers_exact_context_match_over_prefix() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"prefix_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"exact_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"ctx",
SecurityModel::V2c,
SecurityLevel::NoAuthNoPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"exact_view"),
"should prefer exact context match over prefix"
);
}
#[test]
fn test_vacm_access_prefers_longer_context_prefix() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"short_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx_longer"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"long_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"ctx_longer_suffix",
SecurityModel::V2c,
SecurityLevel::NoAuthNoPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"long_view"),
"should prefer longer context prefix"
);
}
#[test]
fn test_vacm_access_prefers_higher_security_level() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"noauth_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::Any,
security_level: SecurityLevel::AuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"auth_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::Any,
security_level: SecurityLevel::AuthPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"authpriv_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"",
SecurityModel::V2c,
SecurityLevel::AuthPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"authpriv_view"),
"should prefer higher security level"
);
}
#[test]
fn test_vacm_access_preference_tier_ordering() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::AuthPriv, context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"any_prefix_short_high"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::V2c,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"v2c_prefix_short_low"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"ctx_test",
SecurityModel::V2c,
SecurityLevel::AuthPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"v2c_prefix_short_low"),
"tier 1 (specific model) should take precedence over tier 4 (security level)"
);
}
#[test]
fn test_vacm_access_preference_context_match_over_prefix_length() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"context"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"long_prefix_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"short_exact_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"ctx",
SecurityModel::V2c,
SecurityLevel::NoAuthNoPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"short_exact_view"),
"tier 2 (exact match) should take precedence over tier 3 (longer prefix)"
);
}
#[test]
fn test_vacm_access_preference_prefix_length_over_security() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::AuthPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"short_high_sec"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx_test"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"long_low_sec"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"ctx_test_suffix",
SecurityModel::V2c,
SecurityLevel::AuthPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"long_low_sec"),
"tier 3 (longer prefix) should take precedence over tier 4 (security level)"
);
}
#[test]
fn test_vacm_access_all_tiers_combined() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"a"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"entry1"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"a"),
security_model: SecurityModel::V2c,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"entry2"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"a",
SecurityModel::V2c,
SecurityLevel::NoAuthNoPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"entry2"),
"specific model + exact match should win"
);
}
#[test]
fn test_vacm_access_exact_wins_regardless_of_insertion_order() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"exact_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::from_static(b"ctx"),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Prefix,
read_view: Bytes::from_static(b"prefix_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"ctx",
SecurityModel::V2c,
SecurityLevel::NoAuthNoPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"exact_view"),
"exact match should win regardless of insertion order"
);
}
#[test]
fn test_vacm_access_higher_security_wins_regardless_of_insertion_order() {
let mut config = VacmConfig::new();
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::Any,
security_level: SecurityLevel::AuthPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"authpriv_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
config.add_access(VacmAccessEntry {
group_name: Bytes::from_static(b"test_group"),
context_prefix: Bytes::new(),
security_model: SecurityModel::Any,
security_level: SecurityLevel::NoAuthNoPriv,
context_match: ContextMatch::Exact,
read_view: Bytes::from_static(b"noauth_view"),
write_view: Bytes::new(),
notify_view: Bytes::new(),
});
let access = config
.get_access(
b"test_group",
b"",
SecurityModel::V2c,
SecurityLevel::AuthPriv,
)
.expect("should find access entry");
assert_eq!(
access.read_view,
Bytes::from_static(b"authpriv_view"),
"higher security level should win regardless of insertion order"
);
}
#[test]
fn test_check_subtree_empty_view_is_excluded() {
let view = View::new();
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1)),
ViewCheckResult::Excluded
);
}
#[test]
fn test_check_subtree_oid_within_included_subtree() {
let view = View::new().include(oid!(1, 3, 6, 1, 2, 1));
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 1, 0)),
ViewCheckResult::Included
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1)),
ViewCheckResult::Included
);
}
#[test]
fn test_check_subtree_oid_within_excluded_subtree() {
let view = View::new()
.include(oid!(1, 3, 6, 1, 2, 1))
.exclude(oid!(1, 3, 6, 1, 2, 1, 1, 7));
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 1, 7, 0)),
ViewCheckResult::Excluded
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 1, 7)),
ViewCheckResult::Excluded
);
}
#[test]
fn test_check_subtree_oid_outside_all_subtrees() {
let view = View::new().include(oid!(1, 3, 6, 1, 2, 1));
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 4, 1)),
ViewCheckResult::Excluded
);
}
#[test]
fn test_check_subtree_parent_of_single_include_is_ambiguous() {
let view = View::new().include(oid!(1, 3, 6, 1, 2, 1));
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1)),
ViewCheckResult::Ambiguous
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6)),
ViewCheckResult::Ambiguous
);
}
#[test]
fn test_check_subtree_parent_of_include_with_nested_exclude() {
let view = View::new()
.include(oid!(1, 3, 6, 1, 2, 1))
.exclude(oid!(1, 3, 6, 1, 2, 1, 1, 7));
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1)),
ViewCheckResult::Ambiguous
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1)),
ViewCheckResult::Ambiguous
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 1)),
ViewCheckResult::Ambiguous
);
}
#[test]
fn test_check_subtree_fully_included_child() {
let view = View::new()
.include(oid!(1, 3, 6, 1, 2, 1))
.exclude(oid!(1, 3, 6, 1, 2, 1, 25));
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 1, 1)),
ViewCheckResult::Included
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 1)),
ViewCheckResult::Included
);
}
#[test]
fn test_check_subtree_multiple_includes() {
let view = View::new()
.include(oid!(1, 3, 6, 1, 2, 1, 1)) .include(oid!(1, 3, 6, 1, 2, 1, 2));
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1)),
ViewCheckResult::Ambiguous
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 1)),
ViewCheckResult::Included
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 2)),
ViewCheckResult::Included
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 3)),
ViewCheckResult::Excluded
);
}
#[test]
fn test_check_subtree_exclude_only_is_excluded() {
let view = View::new().exclude(oid!(1, 3, 6, 1, 2, 1, 1, 7));
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1)),
ViewCheckResult::Excluded
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1)),
ViewCheckResult::Excluded
);
}
#[test]
fn test_check_subtree_with_mask() {
let view = View::new().include_masked(
oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2), vec![0xFF, 0xC0], );
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1)),
ViewCheckResult::Included
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 2)),
ViewCheckResult::Ambiguous
);
assert_eq!(
view.check_subtree(&oid!(1, 3, 6, 1, 2, 1, 2, 2, 1, 3)),
ViewCheckResult::Excluded
);
}
#[test]
fn test_check_subtree_vs_contains_consistency() {
let view = View::new()
.include(oid!(1, 3, 6, 1, 2, 1))
.exclude(oid!(1, 3, 6, 1, 2, 1, 25));
let test_cases = [
oid!(1, 3, 6, 1, 2, 1, 1, 0), oid!(1, 3, 6, 1, 2, 1, 25, 1), oid!(1, 3, 6, 1, 4, 1), ];
for oid in &test_cases {
let check_result = view.check_subtree(oid);
let contains_result = view.contains(oid);
match check_result {
ViewCheckResult::Included => {
assert!(
contains_result,
"check_subtree=Included but contains=false for {:?}",
oid
);
}
ViewCheckResult::Excluded => {
assert!(
!contains_result,
"check_subtree=Excluded but contains=true for {:?}",
oid
);
}
ViewCheckResult::Ambiguous => {
}
}
}
}
}